From 63a0468a46f5a820c69063707375a0ec6354cc69 Mon Sep 17 00:00:00 2001 From: brandon chen <9735006+brandonkachen@users.noreply.github.com> Date: Mon, 4 May 2026 14:17:10 -0700 Subject: [PATCH 01/14] [codex] Fix CLI OAuth login polling (#586) Co-authored-by: James Grugett --- .../web/src/app/api/auth/cli/status/_db.ts | 44 + .../web/src/app/api/auth/cli/status/_get.ts | 101 + .../web/src/app/api/auth/cli/status/route.ts | 114 +- freebuff/web/src/app/onboard/_db.ts | 27 +- freebuff/web/src/app/onboard/_helpers.ts | 3 +- freebuff/web/src/app/onboard/page.tsx | 4 +- .../src/db/migrations/0048_wide_blob.sql | 1 + .../db/migrations/0049_loud_madame_masque.sql | 1 + .../src/db/migrations/meta/0048_snapshot.json | 3168 ++++++++++++++++ .../src/db/migrations/meta/0049_snapshot.json | 3191 +++++++++++++++++ .../src/db/migrations/meta/_journal.json | 16 +- packages/internal/src/db/schema.ts | 31 +- .../auth/cli/status/__tests__/status.test.ts | 137 + web/src/app/api/auth/cli/status/_db.ts | 44 + web/src/app/api/auth/cli/status/_get.ts | 101 + web/src/app/api/auth/cli/status/route.ts | 123 +- web/src/app/onboard/__tests__/helpers.test.ts | 10 +- web/src/app/onboard/_db.ts | 28 +- web/src/app/onboard/_helpers.ts | 3 +- web/src/app/onboard/page.tsx | 13 +- 20 files changed, 6883 insertions(+), 277 deletions(-) create mode 100644 freebuff/web/src/app/api/auth/cli/status/_db.ts create mode 100644 freebuff/web/src/app/api/auth/cli/status/_get.ts create mode 100644 packages/internal/src/db/migrations/0048_wide_blob.sql create mode 100644 packages/internal/src/db/migrations/0049_loud_madame_masque.sql create mode 100644 packages/internal/src/db/migrations/meta/0048_snapshot.json create mode 100644 packages/internal/src/db/migrations/meta/0049_snapshot.json create mode 100644 web/src/app/api/auth/cli/status/__tests__/status.test.ts create mode 100644 web/src/app/api/auth/cli/status/_db.ts create mode 100644 web/src/app/api/auth/cli/status/_get.ts diff --git a/freebuff/web/src/app/api/auth/cli/status/_db.ts b/freebuff/web/src/app/api/auth/cli/status/_db.ts new file mode 100644 index 0000000000..49cbb04b5c --- /dev/null +++ b/freebuff/web/src/app/api/auth/cli/status/_db.ts @@ -0,0 +1,44 @@ +import db from '@codebuff/internal/db' +import * as schema from '@codebuff/internal/db/schema' +import { and, eq, gt } from 'drizzle-orm' + +export interface LoginStatusUser { + id: string + email: string | null + name: string | null + authToken: string +} + +export interface LoginStatusDb { + getCliSessionForAuth( + fingerprintId: string, + fingerprintHash: string, + ): Promise +} + +export function createLoginStatusDb(): LoginStatusDb { + return { + getCliSessionForAuth: async (fingerprintId, fingerprintHash) => { + const users = await db + .select({ + id: schema.user.id, + email: schema.user.email, + name: schema.user.name, + authToken: schema.session.sessionToken, + }) + .from(schema.session) + .innerJoin(schema.user, eq(schema.session.userId, schema.user.id)) + .where( + and( + eq(schema.session.fingerprint_id, fingerprintId), + eq(schema.session.cli_auth_hash, fingerprintHash), + eq(schema.session.type, 'cli'), + gt(schema.session.expires, new Date()), + ), + ) + .limit(1) + + return users[0] ?? null + }, + } +} diff --git a/freebuff/web/src/app/api/auth/cli/status/_get.ts b/freebuff/web/src/app/api/auth/cli/status/_get.ts new file mode 100644 index 0000000000..9816e2780d --- /dev/null +++ b/freebuff/web/src/app/api/auth/cli/status/_get.ts @@ -0,0 +1,101 @@ +import { genAuthCode } from '@codebuff/common/util/credentials' +import { NextResponse } from 'next/server' +import { z } from 'zod/v4' + +import type { LoginStatusDb } from './_db' +import type { Logger } from '@codebuff/common/types/contracts/logger' + +export type { LoginStatusDb } from './_db' + +interface GetLoginStatusDeps { + req: Request + db: LoginStatusDb + logger: Logger + secret: string + now?: () => number +} + +const reqSchema = z.object({ + fingerprintId: z.string(), + fingerprintHash: z.string(), + expiresAt: z.coerce.number().finite().int().positive(), +}) + +export async function getLoginStatus({ + req, + db, + logger, + secret, + now = Date.now, +}: GetLoginStatusDeps): Promise { + const { searchParams } = new URL(req.url) + const result = reqSchema.safeParse({ + fingerprintId: searchParams.get('fingerprintId'), + fingerprintHash: searchParams.get('fingerprintHash'), + expiresAt: searchParams.get('expiresAt'), + }) + if (!result.success) { + return NextResponse.json( + { error: 'Invalid query parameters' }, + { status: 400 }, + ) + } + + const { fingerprintId, fingerprintHash, expiresAt } = result.data + + if (now() > expiresAt) { + logger.info( + { fingerprintId, fingerprintHash, expiresAt }, + 'Auth code expired', + ) + return NextResponse.json( + { error: 'Authentication failed' }, + { status: 401 }, + ) + } + + const expectedHash = genAuthCode(fingerprintId, expiresAt.toString(), secret) + if (fingerprintHash !== expectedHash) { + logger.info( + { fingerprintId, fingerprintHash, expectedHash }, + 'Invalid auth code', + ) + return NextResponse.json( + { error: 'Authentication failed' }, + { status: 401 }, + ) + } + + try { + const user = await db.getCliSessionForAuth(fingerprintId, fingerprintHash) + + if (!user) { + logger.info( + { fingerprintId, fingerprintHash }, + 'No active CLI session found for login auth code', + ) + return NextResponse.json( + { error: 'Authentication failed' }, + { status: 401 }, + ) + } + + return NextResponse.json({ + user: { + id: user.id, + name: user.name, + email: user.email, + authToken: user.authToken, + fingerprintId, + fingerprintHash, + }, + message: 'Authentication successful!', + }) + } catch (error) { + logger.error({ error }, 'Error checking login status') + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 }, + ) + } +} diff --git a/freebuff/web/src/app/api/auth/cli/status/route.ts b/freebuff/web/src/app/api/auth/cli/status/route.ts index dff7adbbf7..bba1274b7c 100644 --- a/freebuff/web/src/app/api/auth/cli/status/route.ts +++ b/freebuff/web/src/app/api/auth/cli/status/route.ts @@ -1,114 +1,14 @@ -import { genAuthCode } from '@codebuff/common/util/credentials' -import db from '@codebuff/internal/db' -import * as schema from '@codebuff/internal/db/schema' import { env } from '@codebuff/internal/env' -import { and, eq, gt, or, isNull } from 'drizzle-orm' -import { NextResponse } from 'next/server' -import { z } from 'zod/v4' +import { createLoginStatusDb } from './_db' +import { getLoginStatus } from './_get' import { logger } from '@/util/logger' export async function GET(req: Request) { - const { searchParams } = new URL(req.url) - const reqSchema = z.object({ - fingerprintId: z.string(), - fingerprintHash: z.string(), - expiresAt: z.string().transform(Number), + return getLoginStatus({ + req, + db: createLoginStatusDb(), + logger, + secret: env.NEXTAUTH_SECRET, }) - const result = reqSchema.safeParse({ - fingerprintId: searchParams.get('fingerprintId'), - fingerprintHash: searchParams.get('fingerprintHash'), - expiresAt: searchParams.get('expiresAt'), - }) - if (!result.success) { - return NextResponse.json( - { error: 'Invalid query parameters' }, - { status: 400 }, - ) - } - - const { fingerprintId, fingerprintHash, expiresAt } = result.data - - if (Date.now() > expiresAt) { - logger.info( - { fingerprintId, fingerprintHash, expiresAt }, - 'Auth code expired', - ) - return NextResponse.json( - { error: 'Authentication failed' }, - { status: 401 }, - ) - } - - const expectedHash = genAuthCode( - fingerprintId, - expiresAt.toString(), - env.NEXTAUTH_SECRET, - ) - if (fingerprintHash !== expectedHash) { - logger.info( - { fingerprintId, fingerprintHash, expectedHash }, - 'Invalid auth code', - ) - return NextResponse.json( - { error: 'Authentication failed' }, - { status: 401 }, - ) - } - - try { - const users = await db - .select({ - id: schema.user.id, - email: schema.user.email, - name: schema.user.name, - authToken: schema.session.sessionToken, - }) - .from(schema.user) - .leftJoin(schema.session, eq(schema.user.id, schema.session.userId)) - .leftJoin( - schema.fingerprint, - eq(schema.session.fingerprint_id, schema.fingerprint.id), - ) - .where( - and( - eq(schema.session.fingerprint_id, fingerprintId), - or( - eq(schema.fingerprint.sig_hash, fingerprintHash), - isNull(schema.fingerprint.sig_hash), - ), - gt(schema.session.expires, new Date()), - ), - ) - - if (users.length === 0) { - logger.info( - { fingerprintId, fingerprintHash }, - 'No active session found or fingerprint claimed by another user', - ) - return NextResponse.json( - { error: 'Authentication failed' }, - { status: 401 }, - ) - } - - const user = users[0] - return NextResponse.json({ - user: { - id: user.id, - name: user.name, - email: user.email, - authToken: user.authToken, - fingerprintId, - fingerprintHash, - }, - message: 'Authentication successful!', - }) - } catch (error) { - logger.error({ error }, 'Error checking login status') - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 }, - ) - } } diff --git a/freebuff/web/src/app/onboard/_db.ts b/freebuff/web/src/app/onboard/_db.ts index 31bcd7c92b..078d757d59 100644 --- a/freebuff/web/src/app/onboard/_db.ts +++ b/freebuff/web/src/app/onboard/_db.ts @@ -1,7 +1,7 @@ import { MAX_DATE } from '@codebuff/common/old-constants' import { db } from '@codebuff/internal/db' import * as schema from '@codebuff/internal/db/schema' -import { and, eq, gt, isNull } from 'drizzle-orm' +import { and, eq, gt, isNull, ne } from 'drizzle-orm' import { cookies } from 'next/headers' import { logger } from '@/util/logger' @@ -12,22 +12,19 @@ type DbTransaction = Parameters[0] extends ( ? T : never -export async function checkReplayAttack( +export async function hasCliSessionForAuthHash( fingerprintHash: string, userId: string, ): Promise { const existing = await db - .select({ id: schema.user.id }) - .from(schema.user) - .leftJoin(schema.session, eq(schema.user.id, schema.session.userId)) - .leftJoin( - schema.fingerprint, - eq(schema.session.fingerprint_id, schema.fingerprint.id), - ) + .select({ id: schema.session.userId }) + .from(schema.session) .where( and( - eq(schema.fingerprint.sig_hash, fingerprintHash), - eq(schema.user.id, userId), + eq(schema.session.cli_auth_hash, fingerprintHash), + eq(schema.session.userId, userId), + eq(schema.session.type, 'cli'), + gt(schema.session.expires, new Date()), ), ) .limit(1) @@ -42,19 +39,19 @@ export async function checkFingerprintConflict( const existingSession = await db .select({ userId: schema.session.userId, - expires: schema.session.expires, }) .from(schema.session) .where( and( eq(schema.session.fingerprint_id, fingerprintId), + ne(schema.session.userId, userId), gt(schema.session.expires, new Date()), ), ) .limit(1) const activeSession = existingSession[0] - if (activeSession && activeSession.userId !== userId) { + if (activeSession) { return { hasConflict: true, existingUserId: activeSession.userId } } return { hasConflict: false } @@ -80,7 +77,7 @@ export async function createCliSession( return db.transaction(async (tx: DbTransaction) => { await tx .insert(schema.fingerprint) - .values({ sig_hash: fingerprintHash, id: fingerprintId }) + .values({ id: fingerprintId }) .onConflictDoNothing() const session = await tx @@ -90,8 +87,10 @@ export async function createCliSession( userId, expires: MAX_DATE, fingerprint_id: fingerprintId, + cli_auth_hash: fingerprintHash, type: 'cli', }) + .onConflictDoNothing() .returning({ userId: schema.session.userId }) if (sessionToken) { diff --git a/freebuff/web/src/app/onboard/_helpers.ts b/freebuff/web/src/app/onboard/_helpers.ts index 68ca3b0401..e26a93d679 100644 --- a/freebuff/web/src/app/onboard/_helpers.ts +++ b/freebuff/web/src/app/onboard/_helpers.ts @@ -20,5 +20,6 @@ export function validateAuthCode( } export function isAuthCodeExpired(expiresAt: string): boolean { - return expiresAt < Date.now().toString() + const expiresAtMs = Number(expiresAt) + return !Number.isFinite(expiresAtMs) || expiresAtMs < Date.now() } diff --git a/freebuff/web/src/app/onboard/page.tsx b/freebuff/web/src/app/onboard/page.tsx index 2299b77ac0..69dba72846 100644 --- a/freebuff/web/src/app/onboard/page.tsx +++ b/freebuff/web/src/app/onboard/page.tsx @@ -6,9 +6,9 @@ import { getServerSession } from 'next-auth' import { checkFingerprintConflict, - checkReplayAttack, createCliSession, getSessionTokenFromCookies, + hasCliSessionForAuthHash, } from './_db' import { isAuthCodeExpired, parseAuthCode, validateAuthCode } from './_helpers' import { authOptions } from '../api/auth/[...nextauth]/auth-options' @@ -119,7 +119,7 @@ const Onboard = async ({ searchParams }: PageProps) => { ) } - const isReplay = await checkReplayAttack(fingerprintHash, user.id) + const isReplay = await hasCliSessionForAuthHash(fingerprintHash, user.id) if (isReplay) { return ( = 1 THEN ancestor_run_ids[1] ELSE id END", + "type": "stored" + } + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END", + "type": "stored" + } + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)", + "type": "stored" + } + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "total_steps": { + "name": "total_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "direct_credits": { + "name": "direct_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_credits": { + "name": "total_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_run_user_id": { + "name": "idx_agent_run_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_parent": { + "name": "idx_agent_run_parent", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_root": { + "name": "idx_agent_run_root", + "columns": [ + { + "expression": "root_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_agent_id": { + "name": "idx_agent_run_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_publisher": { + "name": "idx_agent_run_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_status": { + "name": "idx_agent_run_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_ancestors_gin": { + "name": "idx_agent_run_ancestors_gin", + "columns": [ + { + "expression": "ancestor_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_agent_run_completed_publisher_agent": { + "name": "idx_agent_run_completed_publisher_agent", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_recent": { + "name": "idx_agent_run_completed_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_version": { + "name": "idx_agent_run_completed_version", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_user": { + "name": "idx_agent_run_completed_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_user_id_user_id_fk": { + "name": "agent_run_user_id_user_id_fk", + "tableFrom": "agent_run", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_step": { + "name": "agent_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "credits": { + "name": "credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "child_run_ids": { + "name": "child_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spawned_count": { + "name": "spawned_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "array_length(child_run_ids, 1)", + "type": "stored" + } + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "agent_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_step_number_per_run": { + "name": "unique_step_number_per_run", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_run_id": { + "name": "idx_agent_step_run_id", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_children_gin": { + "name": "idx_agent_step_children_gin", + "columns": [ + { + "expression": "child_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "agent_step_agent_run_id_agent_run_id_fk": { + "name": "agent_step_agent_run_id_agent_run_id_fk", + "tableFrom": "agent_step", + "tableTo": "agent_run", + "columnsFrom": ["agent_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_ledger": { + "name": "credit_ledger", + "schema": "", + "columns": { + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_credit_ledger_active_balance": { + "name": "idx_credit_ledger_active_balance", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "balance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_org": { + "name": "idx_credit_ledger_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_subscription": { + "name": "idx_credit_ledger_subscription", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credit_ledger_org_id_org_id_fk": { + "name": "credit_ledger_org_id_org_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_api_keys": { + "name": "encrypted_api_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "api_key_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_api_keys_user_id_user_id_fk": { + "name": "encrypted_api_keys_user_id_user_id_fk", + "tableFrom": "encrypted_api_keys", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_api_keys_user_id_type_pk": { + "name": "encrypted_api_keys_user_id_type_pk", + "columns": ["user_id", "type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session": { + "name": "free_session", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "free_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "active_instance_id": { + "name": "active_instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cf_country": { + "name": "cf_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geoip_country": { + "name": "geoip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_block_reason": { + "name": "country_block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_privacy_signals": { + "name": "ip_privacy_signals", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "client_ip_hash": { + "name": "client_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_checked_at": { + "name": "country_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_session_queue": { + "name": "idx_free_session_queue", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_free_session_expiry": { + "name": "idx_free_session_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_user_id_user_id_fk": { + "name": "free_session_user_id_user_id_fk", + "tableFrom": "free_session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session_admit": { + "name": "free_session_admit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_session_admit_user_model_time": { + "name": "idx_free_session_admit_user_model_time", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "admitted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_admit_user_id_user_id_fk": { + "name": "free_session_admit_user_id_user_id_fk", + "tableFrom": "free_session_admit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_eval_results": { + "name": "git_eval_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cost_mode": { + "name": "cost_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoner_model": { + "name": "reasoner_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_model": { + "name": "agent_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limit_override": { + "name": "limit_override", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credits_per_block": { + "name": "credits_per_block", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_duration_hours": { + "name": "block_duration_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weekly_credit_limit": { + "name": "weekly_credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "limit_override_user_id_user_id_fk": { + "name": "limit_override_user_id_user_id_fk", + "tableFrom": "limit_override", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "reasoning_text": { + "name": "reasoning_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byok": { + "name": "byok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttft_ms": { + "name": "ttft_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_user_id_idx": { + "name": "message_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_finished_at_user_id_idx": { + "name": "message_finished_at_user_id_idx", + "columns": [ + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_idx": { + "name": "message_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_finished_at_idx": { + "name": "message_org_id_finished_at_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_org_id_org_id_fk": { + "name": "message_org_id_org_id_fk", + "tableFrom": "message", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org": { + "name": "org", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_limit": { + "name": "credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_alerts": { + "name": "billing_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weekly_reports": { + "name": "weekly_reports", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_owner_id_user_id_fk": { + "name": "org_owner_id_user_id_fk", + "tableFrom": "org", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_slug_unique": { + "name": "org_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + }, + "org_stripe_customer_id_unique": { + "name": "org_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": ["stripe_customer_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_feature": { + "name": "org_feature", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_feature_active": { + "name": "idx_org_feature_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_feature_org_id_org_id_fk": { + "name": "org_feature_org_id_org_id_fk", + "tableFrom": "org_feature", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_feature_org_id_feature_pk": { + "name": "org_feature_org_id_feature_pk", + "columns": ["org_id", "feature"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invite": { + "name": "org_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_org_invite_token": { + "name": "idx_org_invite_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_email": { + "name": "idx_org_invite_email", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_expires": { + "name": "idx_org_invite_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invite_org_id_org_id_fk": { + "name": "org_invite_org_id_org_id_fk", + "tableFrom": "org_invite", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invite_invited_by_user_id_fk": { + "name": "org_invite_invited_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "org_invite_accepted_by_user_id_fk": { + "name": "org_invite_accepted_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": ["accepted_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invite_token_unique": { + "name": "org_invite_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_member": { + "name": "org_member", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_member_org_id_org_id_fk": { + "name": "org_member_org_id_org_id_fk", + "tableFrom": "org_member", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_member_user_id_user_id_fk": { + "name": "org_member_user_id_user_id_fk", + "tableFrom": "org_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_member_org_id_user_id_pk": { + "name": "org_member_org_id_user_id_pk", + "columns": ["org_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_repo": { + "name": "org_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_org_repo_active": { + "name": "idx_org_repo_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_repo_unique": { + "name": "idx_org_repo_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_repo_org_id_org_id_fk": { + "name": "org_repo_org_id_org_id_fk", + "tableFrom": "org_repo", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_repo_approved_by_user_id_fk": { + "name": "org_repo_approved_by_user_id_fk", + "tableFrom": "org_repo", + "tableTo": "user", + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publisher": { + "name": "publisher", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "publisher_user_id_user_id_fk": { + "name": "publisher_user_id_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_org_id_org_id_fk": { + "name": "publisher_org_id_org_id_fk", + "tableFrom": "publisher", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_created_by_user_id_fk": { + "name": "publisher_created_by_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "publisher_single_owner": { + "name": "publisher_single_owner", + "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_legacy": { + "name": "is_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": ["referrer_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": ["referred_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": ["referrer_id", "referred_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_auth_hash": { + "name": "cli_auth_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "session_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": ["fingerprint_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scheduled_tier": { + "name": "scheduled_tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_customer": { + "name": "idx_subscription_customer", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_user": { + "name": "idx_subscription_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_status": { + "name": "idx_subscription_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_user_id_user_id_fk": { + "name": "subscription_user_id_user_id_fk", + "tableFrom": "subscription", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_failure": { + "name": "sync_failure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_failure_retry": { + "name": "idx_sync_failure_retry", + "columns": [ + { + "expression": "retry_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sync_failure\".\"retry_count\" < 5", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + }, + "referral_limit": { + "name": "referral_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fallback_to_a_la_carte": { + "name": "fallback_to_a_la_carte", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": ["stripe_customer_id"] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": ["referral_code"] + }, + "user_discord_id_unique": { + "name": "user_discord_id_unique", + "nullsNotDistinct": false, + "columns": ["discord_id"] + }, + "user_handle_unique": { + "name": "user_handle_unique", + "nullsNotDistinct": false, + "columns": ["handle"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": ["identifier", "token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": ["pending", "completed"] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": ["running", "completed", "failed", "cancelled"] + }, + "public.agent_step_status": { + "name": "agent_step_status", + "schema": "public", + "values": ["running", "completed", "skipped"] + }, + "public.api_key_type": { + "name": "api_key_type", + "schema": "public", + "values": ["anthropic", "gemini", "openai"] + }, + "public.free_session_status": { + "name": "free_session_status", + "schema": "public", + "values": ["queued", "active"] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "free", + "referral", + "referral_legacy", + "subscription", + "purchase", + "admin", + "organization", + "ad" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": ["owner", "admin", "member"] + }, + "public.session_type": { + "name": "session_type", + "schema": "public", + "values": ["web", "pat", "cli"] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "incomplete", + "incomplete_expired", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "paused" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/internal/src/db/migrations/meta/0049_snapshot.json b/packages/internal/src/db/migrations/meta/0049_snapshot.json new file mode 100644 index 0000000000..4d8d16ad58 --- /dev/null +++ b/packages/internal/src/db/migrations/meta/0049_snapshot.json @@ -0,0 +1,3191 @@ +{ + "id": "927c6e1e-457f-4815-99d1-96701792e9e5", + "prevId": "4dd02542-1774-450a-a9d0-e342183eab7c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": ["provider", "providerAccountId"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ad_impression": { + "name": "ad_impression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gravity'" + }, + "ad_text": { + "name": "ad_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta": { + "name": "cta", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_url": { + "name": "click_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imp_url": { + "name": "imp_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extra_pixels": { + "name": "extra_pixels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "payout": { + "name": "payout", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "credits_granted": { + "name": "credits_granted", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_operation_id": { + "name": "grant_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "served_at": { + "name": "served_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "impression_fired_at": { + "name": "impression_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clicked_at": { + "name": "clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_ad_impression_user": { + "name": "idx_ad_impression_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ad_impression_imp_url": { + "name": "idx_ad_impression_imp_url", + "columns": [ + { + "expression": "imp_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ad_impression_user_id_user_id_fk": { + "name": "ad_impression_user_id_user_id_fk", + "tableFrom": "ad_impression", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ad_impression_imp_url_unique": { + "name": "ad_impression_imp_url_unique", + "nullsNotDistinct": false, + "columns": ["imp_url"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config": { + "name": "agent_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 1) AS INTEGER)", + "type": "stored" + } + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 2) AS INTEGER)", + "type": "stored" + } + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 3) AS INTEGER)", + "type": "stored" + } + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_config_publisher": { + "name": "idx_agent_config_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_publisher_id_publisher_id_fk": { + "name": "agent_config_publisher_id_publisher_id_fk", + "tableFrom": "agent_config", + "tableTo": "publisher", + "columnsFrom": ["publisher_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_config_publisher_id_id_version_pk": { + "name": "agent_config_publisher_id_id_version_pk", + "columns": ["publisher_id", "id", "version"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run": { + "name": "agent_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '/', 1)\n ELSE NULL\n END", + "type": "stored" + } + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(split_part(agent_id, '/', 2), '@', 1)\n ELSE agent_id\n END", + "type": "stored" + } + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '@', 2)\n ELSE NULL\n END", + "type": "stored" + } + }, + "ancestor_run_ids": { + "name": "ancestor_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "root_run_id": { + "name": "root_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[1] ELSE id END", + "type": "stored" + } + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END", + "type": "stored" + } + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)", + "type": "stored" + } + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "total_steps": { + "name": "total_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "direct_credits": { + "name": "direct_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_credits": { + "name": "total_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_run_user_id": { + "name": "idx_agent_run_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_parent": { + "name": "idx_agent_run_parent", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_root": { + "name": "idx_agent_run_root", + "columns": [ + { + "expression": "root_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_agent_id": { + "name": "idx_agent_run_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_publisher": { + "name": "idx_agent_run_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_status": { + "name": "idx_agent_run_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_ancestors_gin": { + "name": "idx_agent_run_ancestors_gin", + "columns": [ + { + "expression": "ancestor_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_agent_run_completed_publisher_agent": { + "name": "idx_agent_run_completed_publisher_agent", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_recent": { + "name": "idx_agent_run_completed_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_version": { + "name": "idx_agent_run_completed_version", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_user": { + "name": "idx_agent_run_completed_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_user_id_user_id_fk": { + "name": "agent_run_user_id_user_id_fk", + "tableFrom": "agent_run", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_step": { + "name": "agent_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "credits": { + "name": "credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "child_run_ids": { + "name": "child_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spawned_count": { + "name": "spawned_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "array_length(child_run_ids, 1)", + "type": "stored" + } + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "agent_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_step_number_per_run": { + "name": "unique_step_number_per_run", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_run_id": { + "name": "idx_agent_step_run_id", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_children_gin": { + "name": "idx_agent_step_children_gin", + "columns": [ + { + "expression": "child_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "agent_step_agent_run_id_agent_run_id_fk": { + "name": "agent_step_agent_run_id_agent_run_id_fk", + "tableFrom": "agent_step", + "tableTo": "agent_run", + "columnsFrom": ["agent_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_ledger": { + "name": "credit_ledger", + "schema": "", + "columns": { + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_credit_ledger_active_balance": { + "name": "idx_credit_ledger_active_balance", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "balance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_org": { + "name": "idx_credit_ledger_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_subscription": { + "name": "idx_credit_ledger_subscription", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credit_ledger_org_id_org_id_fk": { + "name": "credit_ledger_org_id_org_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_api_keys": { + "name": "encrypted_api_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "api_key_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_api_keys_user_id_user_id_fk": { + "name": "encrypted_api_keys_user_id_user_id_fk", + "tableFrom": "encrypted_api_keys", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_api_keys_user_id_type_pk": { + "name": "encrypted_api_keys_user_id_type_pk", + "columns": ["user_id", "type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session": { + "name": "free_session", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "free_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "active_instance_id": { + "name": "active_instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cf_country": { + "name": "cf_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geoip_country": { + "name": "geoip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_block_reason": { + "name": "country_block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_privacy_signals": { + "name": "ip_privacy_signals", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "client_ip_hash": { + "name": "client_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_checked_at": { + "name": "country_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_session_queue": { + "name": "idx_free_session_queue", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_free_session_expiry": { + "name": "idx_free_session_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_user_id_user_id_fk": { + "name": "free_session_user_id_user_id_fk", + "tableFrom": "free_session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session_admit": { + "name": "free_session_admit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_session_admit_user_model_time": { + "name": "idx_free_session_admit_user_model_time", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "admitted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_admit_user_id_user_id_fk": { + "name": "free_session_admit_user_id_user_id_fk", + "tableFrom": "free_session_admit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_eval_results": { + "name": "git_eval_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cost_mode": { + "name": "cost_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoner_model": { + "name": "reasoner_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_model": { + "name": "agent_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limit_override": { + "name": "limit_override", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credits_per_block": { + "name": "credits_per_block", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_duration_hours": { + "name": "block_duration_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weekly_credit_limit": { + "name": "weekly_credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "limit_override_user_id_user_id_fk": { + "name": "limit_override_user_id_user_id_fk", + "tableFrom": "limit_override", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "reasoning_text": { + "name": "reasoning_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byok": { + "name": "byok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttft_ms": { + "name": "ttft_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_user_id_idx": { + "name": "message_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_finished_at_user_id_idx": { + "name": "message_finished_at_user_id_idx", + "columns": [ + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_idx": { + "name": "message_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_finished_at_idx": { + "name": "message_org_id_finished_at_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_org_id_org_id_fk": { + "name": "message_org_id_org_id_fk", + "tableFrom": "message", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org": { + "name": "org", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_limit": { + "name": "credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_alerts": { + "name": "billing_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weekly_reports": { + "name": "weekly_reports", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_owner_id_user_id_fk": { + "name": "org_owner_id_user_id_fk", + "tableFrom": "org", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_slug_unique": { + "name": "org_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + }, + "org_stripe_customer_id_unique": { + "name": "org_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": ["stripe_customer_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_feature": { + "name": "org_feature", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_feature_active": { + "name": "idx_org_feature_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_feature_org_id_org_id_fk": { + "name": "org_feature_org_id_org_id_fk", + "tableFrom": "org_feature", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_feature_org_id_feature_pk": { + "name": "org_feature_org_id_feature_pk", + "columns": ["org_id", "feature"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invite": { + "name": "org_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_org_invite_token": { + "name": "idx_org_invite_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_email": { + "name": "idx_org_invite_email", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_expires": { + "name": "idx_org_invite_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invite_org_id_org_id_fk": { + "name": "org_invite_org_id_org_id_fk", + "tableFrom": "org_invite", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invite_invited_by_user_id_fk": { + "name": "org_invite_invited_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "org_invite_accepted_by_user_id_fk": { + "name": "org_invite_accepted_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": ["accepted_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invite_token_unique": { + "name": "org_invite_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_member": { + "name": "org_member", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_member_org_id_org_id_fk": { + "name": "org_member_org_id_org_id_fk", + "tableFrom": "org_member", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_member_user_id_user_id_fk": { + "name": "org_member_user_id_user_id_fk", + "tableFrom": "org_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_member_org_id_user_id_pk": { + "name": "org_member_org_id_user_id_pk", + "columns": ["org_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_repo": { + "name": "org_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_org_repo_active": { + "name": "idx_org_repo_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_repo_unique": { + "name": "idx_org_repo_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_repo_org_id_org_id_fk": { + "name": "org_repo_org_id_org_id_fk", + "tableFrom": "org_repo", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_repo_approved_by_user_id_fk": { + "name": "org_repo_approved_by_user_id_fk", + "tableFrom": "org_repo", + "tableTo": "user", + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publisher": { + "name": "publisher", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "publisher_user_id_user_id_fk": { + "name": "publisher_user_id_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_org_id_org_id_fk": { + "name": "publisher_org_id_org_id_fk", + "tableFrom": "publisher", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_created_by_user_id_fk": { + "name": "publisher_created_by_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "publisher_single_owner": { + "name": "publisher_single_owner", + "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_legacy": { + "name": "is_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": ["referrer_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": ["referred_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": ["referrer_id", "referred_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_auth_hash": { + "name": "cli_auth_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "session_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_cli_auth_code_idx": { + "name": "session_cli_auth_code_idx", + "columns": [ + { + "expression": "fingerprint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cli_auth_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"session\".\"cli_auth_hash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": ["fingerprint_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scheduled_tier": { + "name": "scheduled_tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_customer": { + "name": "idx_subscription_customer", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_user": { + "name": "idx_subscription_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_status": { + "name": "idx_subscription_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_user_id_user_id_fk": { + "name": "subscription_user_id_user_id_fk", + "tableFrom": "subscription", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_failure": { + "name": "sync_failure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_failure_retry": { + "name": "idx_sync_failure_retry", + "columns": [ + { + "expression": "retry_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sync_failure\".\"retry_count\" < 5", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + }, + "referral_limit": { + "name": "referral_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fallback_to_a_la_carte": { + "name": "fallback_to_a_la_carte", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": ["stripe_customer_id"] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": ["referral_code"] + }, + "user_discord_id_unique": { + "name": "user_discord_id_unique", + "nullsNotDistinct": false, + "columns": ["discord_id"] + }, + "user_handle_unique": { + "name": "user_handle_unique", + "nullsNotDistinct": false, + "columns": ["handle"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": ["identifier", "token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": ["pending", "completed"] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": ["running", "completed", "failed", "cancelled"] + }, + "public.agent_step_status": { + "name": "agent_step_status", + "schema": "public", + "values": ["running", "completed", "skipped"] + }, + "public.api_key_type": { + "name": "api_key_type", + "schema": "public", + "values": ["anthropic", "gemini", "openai"] + }, + "public.free_session_status": { + "name": "free_session_status", + "schema": "public", + "values": ["queued", "active"] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "free", + "referral", + "referral_legacy", + "subscription", + "purchase", + "admin", + "organization", + "ad" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": ["owner", "admin", "member"] + }, + "public.session_type": { + "name": "session_type", + "schema": "public", + "values": ["web", "pat", "cli"] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "incomplete", + "incomplete_expired", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "paused" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/internal/src/db/migrations/meta/_journal.json b/packages/internal/src/db/migrations/meta/_journal.json index 1b1cd510d1..d93bf88575 100644 --- a/packages/internal/src/db/migrations/meta/_journal.json +++ b/packages/internal/src/db/migrations/meta/_journal.json @@ -337,6 +337,20 @@ "when": 1777317033289, "tag": "0047_tough_silver_fox", "breakpoints": true + }, + { + "idx": 48, + "version": "7", + "when": 1777925902147, + "tag": "0048_wide_blob", + "breakpoints": true + }, + { + "idx": 49, + "version": "7", + "when": 1777929052630, + "tag": "0049_loud_madame_masque", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/internal/src/db/schema.ts b/packages/internal/src/db/schema.ts index 14728a675b..28406296d9 100644 --- a/packages/internal/src/db/schema.ts +++ b/packages/internal/src/db/schema.ts @@ -255,16 +255,27 @@ export const message = pgTable( ], ) -export const session = pgTable('session', { - sessionToken: text('sessionToken').notNull().primaryKey(), - userId: text('userId') - .notNull() - .references(() => user.id, { onDelete: 'cascade' }), - expires: timestamp('expires', { mode: 'date' }).notNull(), - fingerprint_id: text('fingerprint_id').references(() => fingerprint.id), - type: sessionTypeEnum('type').notNull().default('web'), - created_at: timestamp('created_at', { mode: 'date' }).notNull().defaultNow(), -}) +export const session = pgTable( + 'session', + { + sessionToken: text('sessionToken').notNull().primaryKey(), + userId: text('userId') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + expires: timestamp('expires', { mode: 'date' }).notNull(), + fingerprint_id: text('fingerprint_id').references(() => fingerprint.id), + cli_auth_hash: text('cli_auth_hash'), + type: sessionTypeEnum('type').notNull().default('web'), + created_at: timestamp('created_at', { mode: 'date' }) + .notNull() + .defaultNow(), + }, + (table) => [ + uniqueIndex('session_cli_auth_code_idx') + .on(table.fingerprint_id, table.cli_auth_hash) + .where(sql`${table.cli_auth_hash} IS NOT NULL`), + ], +) export const verificationToken = pgTable( 'verificationToken', diff --git a/web/src/app/api/auth/cli/status/__tests__/status.test.ts b/web/src/app/api/auth/cli/status/__tests__/status.test.ts new file mode 100644 index 0000000000..a327d47b80 --- /dev/null +++ b/web/src/app/api/auth/cli/status/__tests__/status.test.ts @@ -0,0 +1,137 @@ +import { genAuthCode } from '@codebuff/common/util/credentials' +import { createMockLogger } from '@codebuff/common/testing/mock-types' +import { describe, expect, mock, test } from 'bun:test' + +import { getLoginStatus } from '../_get' + +import type { LoginStatusDb } from '../_get' + +const secret = 'test-secret' +const fingerprintId = 'enhanced-fingerprint' +const expiresAt = '2000000' + +function createRequest(hash: string): Request { + const params = new URLSearchParams({ + fingerprintId, + fingerprintHash: hash, + expiresAt, + }) + return new Request(`http://localhost/api/auth/cli/status?${params}`) +} + +describe('/api/auth/cli/status', () => { + test('returns the CLI session bound to the current login hash even when an older hash exists', async () => { + const currentHash = genAuthCode(fingerprintId, expiresAt, secret) + const oldHash = genAuthCode(fingerprintId, '1000000', secret) + const getCliSessionForAuth = mock( + async (requestedFingerprintId: string, requestedHash: string) => { + const sessions = [ + { + fingerprintId, + cliAuthHash: oldHash, + type: 'cli', + user: { + id: 'old-user', + email: 'old@example.com', + name: 'Old User', + authToken: 'old-token', + }, + }, + { + fingerprintId, + cliAuthHash: currentHash, + type: 'cli', + user: { + id: 'new-user', + email: 'new@example.com', + name: 'New User', + authToken: 'new-token', + }, + }, + ] + + return ( + sessions.find( + (session) => + session.fingerprintId === requestedFingerprintId && + session.cliAuthHash === requestedHash && + session.type === 'cli', + )?.user ?? null + ) + }, + ) + + const response = await getLoginStatus({ + req: createRequest(currentHash), + db: { getCliSessionForAuth } satisfies LoginStatusDb, + logger: createMockLogger(), + secret, + now: () => 1000000, + }) + + expect(response.status).toBe(200) + const body = await response.json() + expect(body.user.authToken).toBe('new-token') + expect(getCliSessionForAuth).toHaveBeenCalledWith( + fingerprintId, + currentHash, + ) + }) + + test('rejects a wrong login hash', async () => { + const getCliSessionForAuth = mock(async () => ({ + id: 'user', + email: 'user@example.com', + name: 'User', + authToken: 'token', + })) + + const response = await getLoginStatus({ + req: createRequest('wrong-hash'), + db: { getCliSessionForAuth } satisfies LoginStatusDb, + logger: createMockLogger(), + secret, + now: () => 1000000, + }) + + expect(response.status).toBe(401) + expect(getCliSessionForAuth).not.toHaveBeenCalled() + }) + + test('does not authenticate a linked web session', async () => { + const currentHash = genAuthCode(fingerprintId, expiresAt, secret) + const getCliSessionForAuth = mock(async () => null) + + const response = await getLoginStatus({ + req: createRequest(currentHash), + db: { getCliSessionForAuth } satisfies LoginStatusDb, + logger: createMockLogger(), + secret, + now: () => 1000000, + }) + + expect(response.status).toBe(401) + const body = await response.json() + expect(body).toEqual({ error: 'Authentication failed' }) + }) + + test('returns 400 for malformed expiresAt', async () => { + const params = new URLSearchParams({ + fingerprintId, + fingerprintHash: 'hash', + expiresAt: 'not-a-number', + }) + const getCliSessionForAuth = mock(async () => null) + + const response = await getLoginStatus({ + req: new Request(`http://localhost/api/auth/cli/status?${params}`), + db: { getCliSessionForAuth } satisfies LoginStatusDb, + logger: createMockLogger(), + secret, + now: () => 1000000, + }) + + expect(response.status).toBe(400) + expect(getCliSessionForAuth).not.toHaveBeenCalled() + }) +}) diff --git a/web/src/app/api/auth/cli/status/_db.ts b/web/src/app/api/auth/cli/status/_db.ts new file mode 100644 index 0000000000..49cbb04b5c --- /dev/null +++ b/web/src/app/api/auth/cli/status/_db.ts @@ -0,0 +1,44 @@ +import db from '@codebuff/internal/db' +import * as schema from '@codebuff/internal/db/schema' +import { and, eq, gt } from 'drizzle-orm' + +export interface LoginStatusUser { + id: string + email: string | null + name: string | null + authToken: string +} + +export interface LoginStatusDb { + getCliSessionForAuth( + fingerprintId: string, + fingerprintHash: string, + ): Promise +} + +export function createLoginStatusDb(): LoginStatusDb { + return { + getCliSessionForAuth: async (fingerprintId, fingerprintHash) => { + const users = await db + .select({ + id: schema.user.id, + email: schema.user.email, + name: schema.user.name, + authToken: schema.session.sessionToken, + }) + .from(schema.session) + .innerJoin(schema.user, eq(schema.session.userId, schema.user.id)) + .where( + and( + eq(schema.session.fingerprint_id, fingerprintId), + eq(schema.session.cli_auth_hash, fingerprintHash), + eq(schema.session.type, 'cli'), + gt(schema.session.expires, new Date()), + ), + ) + .limit(1) + + return users[0] ?? null + }, + } +} diff --git a/web/src/app/api/auth/cli/status/_get.ts b/web/src/app/api/auth/cli/status/_get.ts new file mode 100644 index 0000000000..9816e2780d --- /dev/null +++ b/web/src/app/api/auth/cli/status/_get.ts @@ -0,0 +1,101 @@ +import { genAuthCode } from '@codebuff/common/util/credentials' +import { NextResponse } from 'next/server' +import { z } from 'zod/v4' + +import type { LoginStatusDb } from './_db' +import type { Logger } from '@codebuff/common/types/contracts/logger' + +export type { LoginStatusDb } from './_db' + +interface GetLoginStatusDeps { + req: Request + db: LoginStatusDb + logger: Logger + secret: string + now?: () => number +} + +const reqSchema = z.object({ + fingerprintId: z.string(), + fingerprintHash: z.string(), + expiresAt: z.coerce.number().finite().int().positive(), +}) + +export async function getLoginStatus({ + req, + db, + logger, + secret, + now = Date.now, +}: GetLoginStatusDeps): Promise { + const { searchParams } = new URL(req.url) + const result = reqSchema.safeParse({ + fingerprintId: searchParams.get('fingerprintId'), + fingerprintHash: searchParams.get('fingerprintHash'), + expiresAt: searchParams.get('expiresAt'), + }) + if (!result.success) { + return NextResponse.json( + { error: 'Invalid query parameters' }, + { status: 400 }, + ) + } + + const { fingerprintId, fingerprintHash, expiresAt } = result.data + + if (now() > expiresAt) { + logger.info( + { fingerprintId, fingerprintHash, expiresAt }, + 'Auth code expired', + ) + return NextResponse.json( + { error: 'Authentication failed' }, + { status: 401 }, + ) + } + + const expectedHash = genAuthCode(fingerprintId, expiresAt.toString(), secret) + if (fingerprintHash !== expectedHash) { + logger.info( + { fingerprintId, fingerprintHash, expectedHash }, + 'Invalid auth code', + ) + return NextResponse.json( + { error: 'Authentication failed' }, + { status: 401 }, + ) + } + + try { + const user = await db.getCliSessionForAuth(fingerprintId, fingerprintHash) + + if (!user) { + logger.info( + { fingerprintId, fingerprintHash }, + 'No active CLI session found for login auth code', + ) + return NextResponse.json( + { error: 'Authentication failed' }, + { status: 401 }, + ) + } + + return NextResponse.json({ + user: { + id: user.id, + name: user.name, + email: user.email, + authToken: user.authToken, + fingerprintId, + fingerprintHash, + }, + message: 'Authentication successful!', + }) + } catch (error) { + logger.error({ error }, 'Error checking login status') + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 }, + ) + } +} diff --git a/web/src/app/api/auth/cli/status/route.ts b/web/src/app/api/auth/cli/status/route.ts index 2053232e4f..bba1274b7c 100644 --- a/web/src/app/api/auth/cli/status/route.ts +++ b/web/src/app/api/auth/cli/status/route.ts @@ -1,123 +1,14 @@ -import { genAuthCode } from '@codebuff/common/util/credentials' -import db from '@codebuff/internal/db' -import * as schema from '@codebuff/internal/db/schema' import { env } from '@codebuff/internal/env' -import { and, eq, gt, or, isNull } from 'drizzle-orm' -import { NextResponse } from 'next/server' -import { z } from 'zod/v4' +import { createLoginStatusDb } from './_db' +import { getLoginStatus } from './_get' import { logger } from '@/util/logger' export async function GET(req: Request) { - const { searchParams } = new URL(req.url) - const reqSchema = z.object({ - fingerprintId: z.string(), - fingerprintHash: z.string(), - expiresAt: z.string().transform(Number), + return getLoginStatus({ + req, + db: createLoginStatusDb(), + logger, + secret: env.NEXTAUTH_SECRET, }) - const result = reqSchema.safeParse({ - fingerprintId: searchParams.get('fingerprintId'), - fingerprintHash: searchParams.get('fingerprintHash'), - expiresAt: searchParams.get('expiresAt'), - }) - if (!result.success) { - return NextResponse.json( - { error: 'Invalid query parameters' }, - { status: 400 }, - ) - } - - const { fingerprintId, fingerprintHash, expiresAt } = result.data - - // Check if code has expired - if (Date.now() > expiresAt) { - logger.info( - { fingerprintId, fingerprintHash, expiresAt }, - 'Auth code expired', - ) - return NextResponse.json( - { error: 'Authentication failed' }, - { status: 401 }, - ) - } - - // Validate the auth code - const expectedHash = genAuthCode( - fingerprintId, - expiresAt.toString(), - env.NEXTAUTH_SECRET, - ) - if (fingerprintHash !== expectedHash) { - logger.info( - { fingerprintId, fingerprintHash, expectedHash }, - 'Invalid auth code', - ) - return NextResponse.json( - { error: 'Authentication failed' }, - { status: 401 }, - ) - } - - try { - const users = await db - .select({ - id: schema.user.id, - email: schema.user.email, - name: schema.user.name, - authToken: schema.session.sessionToken, - }) - .from(schema.user) - .leftJoin(schema.session, eq(schema.user.id, schema.session.userId)) - .leftJoin( - schema.fingerprint, - eq(schema.session.fingerprint_id, schema.fingerprint.id), - ) - .where( - and( - eq(schema.session.fingerprint_id, fingerprintId), - // Allow access if either: - // 1. The fingerprint's sig_hash matches what the user provided (they own it) - // 2. The fingerprint's sig_hash is null (it's unclaimed/abandoned) - or( - eq(schema.fingerprint.sig_hash, fingerprintHash), - isNull(schema.fingerprint.sig_hash), - ), - gt(schema.session.expires, new Date()), // Only return active sessions - ), - ) - - if (users.length === 0) { - // No active session found - either: - // - This is a new fingerprint - // - The fingerprint exists but has no active session - // - The fingerprint is claimed by someone else (sig_hash mismatch) - logger.info( - { fingerprintId, fingerprintHash }, - 'No active session found or fingerprint claimed by another user', - ) - return NextResponse.json( - { error: 'Authentication failed' }, - { status: 401 }, - ) - } - - const user = users[0] - return NextResponse.json({ - user: { - id: user.id, - name: user.name, - email: user.email, - authToken: user.authToken, - fingerprintId, - fingerprintHash, - }, - message: 'Authentication successful!', - }) - } catch (error) { - logger.error({ error }, 'Error checking login status') - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 }, - ) - } } diff --git a/web/src/app/onboard/__tests__/helpers.test.ts b/web/src/app/onboard/__tests__/helpers.test.ts index 8cb02f11d7..8fb96514ba 100644 --- a/web/src/app/onboard/__tests__/helpers.test.ts +++ b/web/src/app/onboard/__tests__/helpers.test.ts @@ -1,7 +1,6 @@ import { genAuthCode } from '@codebuff/common/util/credentials' import { afterEach, beforeEach, describe, expect, test } from 'bun:test' - import { parseAuthCode, validateAuthCode, isAuthCodeExpired } from '../_helpers' describe('onboard/_helpers', () => { @@ -227,17 +226,18 @@ describe('onboard/_helpers', () => { expect(isAuthCodeExpired(notYetExpired)).toBe(false) }) - test('handles string comparison correctly for timestamps', () => { - // The function uses string comparison (expiresAt < Date.now().toString()) - // This tests that it works correctly with numeric strings + test('compares numeric timestamp strings', () => { const fixedNow = 1704067200000 Date.now = () => fixedNow - // String "1704067199999" < "1704067200000" lexicographically (and numerically) expect(isAuthCodeExpired('1704067199999')).toBe(true) expect(isAuthCodeExpired('1704067200001')).toBe(false) }) + test('treats malformed timestamps as expired', () => { + expect(isAuthCodeExpired('not-a-number')).toBe(true) + }) + test('handles very old timestamps', () => { const veryOld = '0' // Epoch expect(isAuthCodeExpired(veryOld)).toBe(true) diff --git a/web/src/app/onboard/_db.ts b/web/src/app/onboard/_db.ts index 3cafc9b9ff..078d757d59 100644 --- a/web/src/app/onboard/_db.ts +++ b/web/src/app/onboard/_db.ts @@ -1,8 +1,7 @@ - import { MAX_DATE } from '@codebuff/common/old-constants' import { db } from '@codebuff/internal/db' import * as schema from '@codebuff/internal/db/schema' -import { and, eq, gt, isNull } from 'drizzle-orm' +import { and, eq, gt, isNull, ne } from 'drizzle-orm' import { cookies } from 'next/headers' import { logger } from '@/util/logger' @@ -13,22 +12,19 @@ type DbTransaction = Parameters[0] extends ( ? T : never -export async function checkReplayAttack( +export async function hasCliSessionForAuthHash( fingerprintHash: string, userId: string, ): Promise { const existing = await db - .select({ id: schema.user.id }) - .from(schema.user) - .leftJoin(schema.session, eq(schema.user.id, schema.session.userId)) - .leftJoin( - schema.fingerprint, - eq(schema.session.fingerprint_id, schema.fingerprint.id), - ) + .select({ id: schema.session.userId }) + .from(schema.session) .where( and( - eq(schema.fingerprint.sig_hash, fingerprintHash), - eq(schema.user.id, userId), + eq(schema.session.cli_auth_hash, fingerprintHash), + eq(schema.session.userId, userId), + eq(schema.session.type, 'cli'), + gt(schema.session.expires, new Date()), ), ) .limit(1) @@ -43,19 +39,19 @@ export async function checkFingerprintConflict( const existingSession = await db .select({ userId: schema.session.userId, - expires: schema.session.expires, }) .from(schema.session) .where( and( eq(schema.session.fingerprint_id, fingerprintId), + ne(schema.session.userId, userId), gt(schema.session.expires, new Date()), ), ) .limit(1) const activeSession = existingSession[0] - if (activeSession && activeSession.userId !== userId) { + if (activeSession) { return { hasConflict: true, existingUserId: activeSession.userId } } return { hasConflict: false } @@ -81,7 +77,7 @@ export async function createCliSession( return db.transaction(async (tx: DbTransaction) => { await tx .insert(schema.fingerprint) - .values({ sig_hash: fingerprintHash, id: fingerprintId }) + .values({ id: fingerprintId }) .onConflictDoNothing() const session = await tx @@ -91,8 +87,10 @@ export async function createCliSession( userId, expires: MAX_DATE, fingerprint_id: fingerprintId, + cli_auth_hash: fingerprintHash, type: 'cli', }) + .onConflictDoNothing() .returning({ userId: schema.session.userId }) if (sessionToken) { diff --git a/web/src/app/onboard/_helpers.ts b/web/src/app/onboard/_helpers.ts index 68ca3b0401..e26a93d679 100644 --- a/web/src/app/onboard/_helpers.ts +++ b/web/src/app/onboard/_helpers.ts @@ -20,5 +20,6 @@ export function validateAuthCode( } export function isAuthCodeExpired(expiresAt: string): boolean { - return expiresAt < Date.now().toString() + const expiresAtMs = Number(expiresAt) + return !Number.isFinite(expiresAtMs) || expiresAtMs < Date.now() } diff --git a/web/src/app/onboard/page.tsx b/web/src/app/onboard/page.tsx index f39d22a208..6e5ea8f883 100644 --- a/web/src/app/onboard/page.tsx +++ b/web/src/app/onboard/page.tsx @@ -4,12 +4,11 @@ import { env } from '@codebuff/internal/env' import { redirect } from 'next/navigation' import { getServerSession } from 'next-auth' - import { checkFingerprintConflict, - checkReplayAttack, createCliSession, getSessionTokenFromCookies, + hasCliSessionForAuthHash, } from './_db' import { isAuthCodeExpired, parseAuthCode, validateAuthCode } from './_helpers' import { authOptions } from '../api/auth/[...nextauth]/auth-options' @@ -18,7 +17,6 @@ import CardWithBeams from '@/components/card-with-beams' import { WelcomeCard } from '@/components/onboard/welcome-card' import { logger } from '@/util/logger' - interface PageProps { searchParams?: Promise<{ auth_code?: string @@ -32,7 +30,12 @@ const Onboard = async ({ searchParams }: PageProps) => { const user = session?.user if (!user) { - return redirect(env.NEXT_PUBLIC_CODEBUFF_APP_URL) + const params = new URLSearchParams() + if (authCode) params.set('auth_code', authCode) + const query = params.toString() + return redirect( + query ? `/login?${query}` : env.NEXT_PUBLIC_CODEBUFF_APP_URL, + ) } if (!authCode) { @@ -83,7 +86,7 @@ const Onboard = async ({ searchParams }: PageProps) => { ) } - const isReplay = await checkReplayAttack(fingerprintHash, user.id) + const isReplay = await hasCliSessionForAuthHash(fingerprintHash, user.id) if (isReplay) { return ( Date: Mon, 4 May 2026 14:56:07 -0700 Subject: [PATCH 02/14] Fix context summary continuation format (#587) --- agents/__tests__/context-pruner.test.ts | 259 +++++++++++++----- agents/context-pruner.ts | 220 ++++++++++----- .../e2e/base2-free-summary-format.e2e.test.ts | 57 ++-- 3 files changed, 387 insertions(+), 149 deletions(-) diff --git a/agents/__tests__/context-pruner.test.ts b/agents/__tests__/context-pruner.test.ts index b691f33a9f..4837740e79 100644 --- a/agents/__tests__/context-pruner.test.ts +++ b/agents/__tests__/context-pruner.test.ts @@ -292,9 +292,12 @@ describe('context-pruner handleSteps', () => { expect(content).toContain('') expect(content).toContain('') - // Should contain the user and assistant markers - expect(content).toContain('[USER]') - expect(content).toContain('[ASSISTANT]') + // Should use a memory artifact format, not transcript role markers + expect(content).toContain('') + expect(content).toContain('User request:') + expect(content).toContain('Progress note:') + expect(content).not.toContain('[USER]') + expect(content).not.toContain('[ASSISTANT]') }) test('includes tool call summaries in the output', () => { @@ -303,7 +306,9 @@ describe('context-pruner handleSteps', () => { createToolCallMessage('call-1', 'read_files', { paths: ['file1.ts', 'file2.ts'], }), - createToolResultMessage('call-1', 'read_files', { content: 'file data' } as JSONValue), + createToolResultMessage('call-1', 'read_files', { + content: 'file data', + } as JSONValue), createMessage('user', 'Now edit this file'), createToolCallMessage('call-2', 'str_replace', { path: 'file1.ts', @@ -316,8 +321,8 @@ describe('context-pruner handleSteps', () => { const content = results[0].input.messages[0].content[0].text // Should contain tool summaries - expect(content).toContain('Read files: file1.ts, file2.ts') - expect(content).toContain('Edited file: file1.ts') + expect(content).toContain('Previously inspected files: file1.ts, file2.ts') + expect(content).toContain('Previously edited file: file1.ts') }) test('summarizes various tool types correctly', () => { @@ -345,10 +350,10 @@ describe('context-pruner handleSteps', () => { const results = runHandleSteps(messages, 50000, 10000) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Wrote file: new-file.ts') - expect(content).toContain('Ran command: npm test') - expect(content).toContain('Code search: "function"') - expect(content).toContain('Spawned agents:') + expect(content).toContain('Previously wrote file: new-file.ts') + expect(content).toContain('Previously ran command: npm test') + expect(content).toContain('Previous code search for "function"') + expect(content).toContain('Previously delegated agents:') expect(content).toContain('- file-picker') expect(content).toContain('- commander') }) @@ -365,7 +370,7 @@ describe('context-pruner handleSteps', () => { const results = runHandleSteps(messages, 50000, 10000) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('[TOOL ERROR: read_files] File not found') + expect(content).toContain('Tool error from read_files: File not found') }) test('notes when user messages have images', () => { @@ -382,7 +387,7 @@ describe('context-pruner handleSteps', () => { const results = runHandleSteps(messages, 50000, 10000) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('[USER] [with image(s)]') + expect(content).toContain('User request [image(s) were attached]:') }) test('removes only INSTRUCTIONS_PROMPT and SUBAGENT_SPAWN when under context limit', () => { @@ -490,6 +495,90 @@ describe('context-pruner handleSteps', () => { expect(instructionsContent).toBe('Parent agent instructions') }) + test('preserves tagged live user prompt as a real message after summary', () => { + const liveUserPrompt: Message = { + role: 'user', + content: [{ type: 'text', text: 'LATEST LIVE REQUEST' }], + tags: ['USER_PROMPT'], + } + const instructionsPrompt: Message = { + role: 'user', + content: [{ type: 'text', text: 'Parent instructions' }], + tags: ['INSTRUCTIONS_PROMPT'], + } + const prunerParamsPrompt: Message = { + role: 'user', + content: [{ type: 'text', text: '{"maxContextLength":200000}' }], + tags: ['USER_PROMPT'], + } + const messages: Message[] = [ + createMessage('user', 'Older request'), + createMessage('assistant', 'Older answer'), + liveUserPrompt, + instructionsPrompt, + prunerParamsPrompt, + ] + + const results = runHandleSteps(messages, 250000, 200000) + const resultMessages = results[0].input.messages + + expect(resultMessages).toHaveLength(2) + const summaryContent = (resultMessages[0].content[0] as { text: string }) + .text + expect(summaryContent).toContain('Older request') + expect(summaryContent).not.toContain('LATEST LIVE REQUEST') + expect(resultMessages[1]).toEqual( + expect.objectContaining({ + role: 'user', + tags: ['USER_PROMPT'], + }), + ) + expect((resultMessages[1].content[0] as { text: string }).text).toBe( + 'LATEST LIVE REQUEST', + ) + }) + + test('keeps live user prompt in memory and adds continuation prompt when pruning mid-turn', () => { + const liveUserPrompt: Message = { + role: 'user', + content: [{ type: 'text', text: 'PLEASE FIX THE BUG' }], + tags: ['USER_PROMPT'], + } + const prunerParamsPrompt: Message = { + role: 'user', + content: [{ type: 'text', text: '{"maxContextLength":200000}' }], + tags: ['USER_PROMPT'], + } + const messages: Message[] = [ + liveUserPrompt, + createMessage('assistant', 'I found the likely issue.'), + createToolCallMessage('call-1', 'read_files', { + paths: ['src/bug.ts'], + }), + createToolResultMessage('call-1', 'read_files', { + content: 'buggy code', + }), + prunerParamsPrompt, + ] + + const results = runHandleSteps(messages, 250000, 200000) + const resultMessages = results[0].input.messages + + expect(resultMessages).toHaveLength(2) + const summaryContent = (resultMessages[0].content[0] as { text: string }) + .text + expect(summaryContent).toContain('PLEASE FIX THE BUG') + expect(summaryContent).toContain('I found the likely issue.') + expect(summaryContent).toContain('Previously inspected files: src/bug.ts') + + expect(resultMessages[1].role).toBe('user') + expect(resultMessages[1].tags).toBeUndefined() + const continuationText = (resultMessages[1].content[0] as { text: string }) + .text + expect(continuationText).toContain('Continue the existing assistant turn') + expect(continuationText).toContain('Do not restart completed work') + }) + test('handles empty message history', () => { const messages: Message[] = [] @@ -564,7 +653,7 @@ describe('context-pruner handleSteps', () => { const results = runHandleSteps(messages, 50000, 10000) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Spawned agent: file-picker') + expect(content).toContain('Previously delegated agent file-picker') }) test('handles long terminal commands by truncating', () => { @@ -583,7 +672,7 @@ describe('context-pruner handleSteps', () => { // Should truncate to 50 chars + ... expect(content).toContain( - 'Ran command: npm run build -- --config=production --verbose --o...', + 'Previously ran command: npm run build -- --config=production --verbose --o...', ) }) @@ -597,7 +686,7 @@ describe('context-pruner handleSteps', () => { const results = runHandleSteps(messages, 50000, 10000) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Used tool: unknown_tool_name') + expect(content).toContain('Previously used tool unknown_tool_name') }) test('handles multiple tool calls in single assistant message', () => { @@ -630,8 +719,8 @@ describe('context-pruner handleSteps', () => { const content = results[0].input.messages[0].content[0].text // Both tool calls should be in the summary - expect(content).toContain('Read files: a.ts') - expect(content).toContain('Read files: b.ts') + expect(content).toContain('Previously inspected files: a.ts') + expect(content).toContain('Previously inspected files: b.ts') }) test('handles mixed text and tool calls in assistant message', () => { @@ -659,7 +748,7 @@ describe('context-pruner handleSteps', () => { // Should have both text and tool summary expect(content).toContain('Let me read that file for you') - expect(content).toContain('Read files: test.ts') + expect(content).toContain('Previously inspected files: test.ts') }) }) @@ -803,7 +892,9 @@ describe('context-pruner code_search with flags', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Code search: "myFunction" (-g *.ts -i)') + expect(content).toContain( + 'Previous code search for "myFunction" (-g *.ts -i)', + ) }) }) @@ -877,7 +968,7 @@ describe('context-pruner ask_user with questions and answers', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('[USER ANSWERED] Option B was selected') + expect(content).toContain('User answered: Option B was selected') }) test('includes multi-select answers', () => { @@ -896,7 +987,7 @@ describe('context-pruner ask_user with questions and answers', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('[USER ANSWERED] Caching, Logging, Monitoring') + expect(content).toContain('User answered: Caching, Logging, Monitoring') }) test('shows when user skipped question', () => { @@ -913,7 +1004,7 @@ describe('context-pruner ask_user with questions and answers', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('[USER SKIPPED QUESTION]') + expect(content).toContain('User skipped question') }) }) @@ -964,7 +1055,7 @@ describe('context-pruner terminal command exit codes', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('[COMMAND FAILED] Exit code: 1') + expect(content).toContain('Command failed with exit code: 1') }) test('does not show failure for successful command (exit code 0)', () => { @@ -982,7 +1073,7 @@ describe('context-pruner terminal command exit codes', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).not.toContain('[COMMAND FAILED]') + expect(content).not.toContain('Command failed with exit code') }) }) @@ -1257,9 +1348,7 @@ First assistant response }) test('keeps multi-part tool entries grouped across compaction cycles', () => { - const simulateCompaction = ( - inputMessages: Message[], - ): Message => { + const simulateCompaction = (inputMessages: Message[]): Message => { const result = runHandleSteps(inputMessages, 250000, 200000) return result[0].input.messages[0] } @@ -1285,8 +1374,10 @@ First assistant response .text // Both parts should be present in cycle 1 - expect(summary1Text).toContain('[TOOL ERROR: run_terminal_command] Test suite failed') - expect(summary1Text).toContain('[COMMAND FAILED] Exit code: 1') + expect(summary1Text).toContain( + 'Tool error from run_terminal_command: Test suite failed', + ) + expect(summary1Text).toContain('Command failed with exit code: 1') // Cycle 2: re-compact — the multi-part entry should stay as one entry const cycle2Messages: Message[] = [ @@ -1299,8 +1390,10 @@ First assistant response .text // Both parts should still be present together after re-compaction - expect(summary2Text).toContain('[TOOL ERROR: run_terminal_command] Test suite failed') - expect(summary2Text).toContain('[COMMAND FAILED] Exit code: 1') + expect(summary2Text).toContain( + 'Tool error from run_terminal_command: Test suite failed', + ) + expect(summary2Text).toContain('Command failed with exit code: 1') // They should be within the same --- delimited chunk (not split apart) const separator = '\n\n---\n\n' @@ -1308,9 +1401,9 @@ First assistant response .replace(/[\s\S]*?\n\n/, '') .replace(/<\/conversation_summary>[\s\S]*/, '') .split(separator) - const errorChunk = chunks.find((c) => c.includes('[TOOL ERROR:')) + const errorChunk = chunks.find((c) => c.includes('Tool error from')) expect(errorChunk).toBeDefined() - expect(errorChunk).toContain('[COMMAND FAILED] Exit code: 1') + expect(errorChunk).toContain('Command failed with exit code: 1') }) test('handles 3+ compaction cycles without nested PREVIOUS SUMMARY markers', () => { @@ -1562,14 +1655,15 @@ describe('context-pruner str_replace and write_file tool results', () => { createToolResultMessage('call-1', 'str_replace', { file: 'src/utils.ts', message: 'Updated file', - unifiedDiff: '--- a/src/utils.ts\n+++ b/src/utils.ts\n@@ -1,1 +1,1 @@\n-foo\n+bar', + unifiedDiff: + '--- a/src/utils.ts\n+++ b/src/utils.ts\n@@ -1,1 +1,1 @@\n-foo\n+bar', }), ] const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('[EDIT RESULT: str_replace]') + expect(content).toContain('Edit result from str_replace:') expect(content).toContain('unifiedDiff') expect(content).toContain('-foo') expect(content).toContain('+bar') @@ -1585,14 +1679,15 @@ describe('context-pruner str_replace and write_file tool results', () => { createToolResultMessage('call-1', 'write_file', { file: 'src/new-file.ts', message: 'Created file', - unifiedDiff: '--- /dev/null\n+++ b/src/new-file.ts\n@@ -0,0 +1 @@\n+export const hello = "world"', + unifiedDiff: + '--- /dev/null\n+++ b/src/new-file.ts\n@@ -0,0 +1 @@\n+export const hello = "world"', }), ] const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('[EDIT RESULT: write_file]') + expect(content).toContain('Edit result from write_file:') expect(content).toContain('export const hello') }) @@ -1614,7 +1709,7 @@ describe('context-pruner str_replace and write_file tool results', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('[EDIT RESULT: str_replace]') + expect(content).toContain('Edit result from str_replace:') expect(content).toContain('...') // Should not contain the full diff expect(content).not.toContain(longDiff) @@ -1680,8 +1775,8 @@ describe('context-pruner str_replace and write_file tool results', () => { const content = results[0].input.messages[0].content[0].text // Should have both the tool call summary and the full result - expect(content).toContain('Edited file: src/file.ts') - expect(content).toContain('[EDIT RESULT: str_replace]') + expect(content).toContain('Previously edited file: src/file.ts') + expect(content).toContain('Edit result from str_replace:') expect(content).toContain('errorMessage') expect(content).toContain('No match found for old string') }) @@ -1731,7 +1826,7 @@ describe('context-pruner glob and list_directory tools', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Glob: **/*.ts') + expect(content).toContain('Previous glob search for **/*.ts') }) test('summarizes list_directory tool with path', () => { @@ -1746,7 +1841,7 @@ describe('context-pruner glob and list_directory tools', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Listed dir: src') + expect(content).toContain('Previously listed directory: src') }) test('summarizes read_subtree tool with paths', () => { @@ -1761,7 +1856,9 @@ describe('context-pruner glob and list_directory tools', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Read subtree: src/components, src/utils') + expect(content).toContain( + 'Previously inspected subtrees: src/components, src/utils', + ) }) }) @@ -1920,17 +2017,24 @@ describe('context-pruner dual-budget behavior', () => { }) test('counts tool result summaries against assistant+tool budget', () => { - // Use str_replace with a large result — this produces a summarized [EDIT RESULT] entry + // Use str_replace with a large result — this produces a summarized edit-result entry const largeDiff = 'LARGE_DIFF_CONTENT_' + 'X'.repeat(900) const messages = [ createMessage('user', 'Do something'), - createToolCallMessage('call-1', 'str_replace', { path: 'big.ts', replacements: [] }), - createToolResultMessage('call-1', 'str_replace', { file: 'big.ts', message: 'Updated', unifiedDiff: largeDiff }), + createToolCallMessage('call-1', 'str_replace', { + path: 'big.ts', + replacements: [], + }), + createToolResultMessage('call-1', 'str_replace', { + file: 'big.ts', + message: 'Updated', + unifiedDiff: largeDiff, + }), createMessage('user', 'Recent question'), createMessage('assistant', 'Recent answer'), ] - // Assistant budget too small for the large [EDIT RESULT] summary entry + // Assistant budget too small for the large edit-result summary entry const results = runHandleSteps(messages, 250000, 200000, { assistantToolBudget: 100, userBudget: 5000, @@ -2133,11 +2237,23 @@ describe('context-pruner dual-budget behavior', () => { // Long user message (~45k chars, exceeds USER_MESSAGE_LIMIT of 13k tokens = 39k chars) // Middle marker placed ~85% through so it falls in the truncated gap // (past the 80% prefix but before the 20% suffix) - const longUserMessage = 'LONG_USER_START_' + 'Here is a detailed specification for the new feature. '.repeat(650) + '_LONG_USER_MIDDLE_MARKER_' + 'Here is a detailed specification for the new feature. '.repeat(150) + const longUserMessage = + 'LONG_USER_START_' + + 'Here is a detailed specification for the new feature. '.repeat(650) + + '_LONG_USER_MIDDLE_MARKER_' + + 'Here is a detailed specification for the new feature. '.repeat(150) // Long assistant message with text (~8k chars, exceeds ASSISTANT_MESSAGE_LIMIT of 1.3k tokens = 3.9k chars) // plus multiple tool calls. Middle marker placed ~60% through so it falls in the truncated gap. - const longAssistantText = 'LONG_ASSISTANT_START_' + 'I will implement this step by step, starting with the data model changes. '.repeat(60) + '_LONG_ASST_MIDDLE_MARKER_' + 'I will implement this step by step, starting with the data model changes. '.repeat(40) + const longAssistantText = + 'LONG_ASSISTANT_START_' + + 'I will implement this step by step, starting with the data model changes. '.repeat( + 60, + ) + + '_LONG_ASST_MIDDLE_MARKER_' + + 'I will implement this step by step, starting with the data model changes. '.repeat( + 40, + ) const assistantWithToolCalls: Message = { role: 'assistant', content: [ @@ -2172,7 +2288,8 @@ describe('context-pruner dual-budget behavior', () => { } // str_replace result with a large diff (~3k chars, exceeds 2k truncation limit) - const largeDiff = 'DIFF_START_MARKER_' + '+added line\n'.repeat(250) + '_DIFF_END_MARKER' + const largeDiff = + 'DIFF_START_MARKER_' + '+added line\n'.repeat(250) + '_DIFF_END_MARKER' // spawn_agents result with 5 non-blacklisted agents producing large outputs // Each ~4k chars, total ~20k, exceeds TOOL_ENTRY_LIMIT of 5k tokens = 15k chars @@ -2180,7 +2297,10 @@ describe('context-pruner dual-budget behavior', () => { agentType: 'editor', value: { type: 'string', - value: `AGENT_${i}_OUTPUT_START_` + 'Implementation details. '.repeat(160) + `_AGENT_${i}_OUTPUT_END`, + value: + `AGENT_${i}_OUTPUT_START_` + + 'Implementation details. '.repeat(160) + + `_AGENT_${i}_OUTPUT_END`, }, })) @@ -2188,8 +2308,14 @@ describe('context-pruner dual-budget behavior', () => { previousSummary, createMessage('user', longUserMessage), assistantWithToolCalls, - createToolResultMessage('call-1', 'read_files', { content: 'file data' } as JSONValue), - createToolResultMessage('call-2', 'str_replace', { file: 'src/model.ts', message: 'Updated', unifiedDiff: largeDiff }), + createToolResultMessage('call-1', 'read_files', { + content: 'file data', + } as JSONValue), + createToolResultMessage('call-2', 'str_replace', { + file: 'src/model.ts', + message: 'Updated', + unifiedDiff: largeDiff, + }), { role: 'tool', toolCallId: 'call-3', @@ -2210,7 +2336,8 @@ describe('context-pruner dual-budget behavior', () => { // === Structure checks === expect(content).toContain('') expect(content).toContain('') - const summaryTagCount = (content.match(//g) || []).length + const summaryTagCount = (content.match(//g) || []) + .length expect(summaryTagCount).toBe(1) // === Previous summary entries preserved === @@ -2229,12 +2356,14 @@ describe('context-pruner dual-budget behavior', () => { expect(content).not.toContain('_LONG_ASST_MIDDLE_MARKER_') // Middle marker falls in truncated gap // === Tool call summaries present === - expect(content).toContain('Read files: src/model.ts, src/service.ts') - expect(content).toContain('Edited file: src/model.ts') - expect(content).toContain('Spawned agents:') + expect(content).toContain( + 'Previously inspected files: src/model.ts, src/service.ts', + ) + expect(content).toContain('Previously edited file: src/model.ts') + expect(content).toContain('Previously delegated agents:') // === str_replace result: present but truncated at 2k chars === - expect(content).toContain('[EDIT RESULT: str_replace]') + expect(content).toContain('Edit result from str_replace:') expect(content).toContain('DIFF_START_MARKER_') expect(content).not.toContain('_DIFF_END_MARKER') // Truncated by 2k result limit @@ -2258,13 +2387,16 @@ describe('context-pruner dual-budget behavior', () => { content: [ { type: 'text', - text: `\nThis is a summary of the conversation so far. The original messages have been condensed to save context space.\n\n[USER]\nOLD_DROPPED_USER: ${'X'.repeat(600)}\n\n---\n\n[ASSISTANT]\nOLD_DROPPED_ASSISTANT: ${'Y'.repeat(600)}\n\n---\n\n[USER]\nOLD_DROPPED_USER_2: Asked about deployment\n\n---\n\n[ASSISTANT]\nOLD_DROPPED_ASSISTANT_2: Explained deployment process\n`, + text: `\nThis is a summary of the conversation so far. The original messages have been condensed to save context space.\n\n[USER]\nOLD_DROPPED_USER: ${'X'.repeat(600)}\n\n---\n\n[ASSISTANT]\nOLD_DROPPED_ASSISTANT: ${'Y'.repeat(600)}\n\n---\n\n[USER]\nOLD_DROPPED_USER_2: Asked about deployment\n\n---\n\n[ASSISTANT]\nOLD_DROPPED_ASSISTANT_2: ${'Explained deployment process. '.repeat(80)}\n`, }, ], } // Long user message (~12k chars, under truncation limit but uses significant budget) - const longUserMessage = 'SURVIVED_USER_START_' + 'Feature request details. '.repeat(400) + '_SURVIVED_USER_END' + const longUserMessage = + 'SURVIVED_USER_START_' + + 'Feature request details. '.repeat(400) + + '_SURVIVED_USER_END' // Assistant with tool calls const assistantMsg: Message = { @@ -2284,7 +2416,8 @@ describe('context-pruner dual-budget behavior', () => { const toolResult = createToolResultMessage('call-1', 'str_replace', { file: 'src/app.ts', message: 'Updated file', - unifiedDiff: '--- a/src/app.ts\n+++ b/src/app.ts\n@@ -1 +1 @@\n-old\n+SURVIVED_DIFF_CONTENT', + unifiedDiff: + '--- a/src/app.ts\n+++ b/src/app.ts\n@@ -1 +1 @@\n-old\n+SURVIVED_DIFF_CONTENT', }) const messages: Message[] = [ @@ -2300,8 +2433,8 @@ describe('context-pruner dual-budget behavior', () => { // New assistant entries: ~25 (assistant text+tool) + ~56 (edit result JSON) + ~13 (final) = ~94 tokens // Old assistant entries: ~20 for OLD_DROPPED_ASSISTANT_2 would push over budget of 100 const results = runHandleSteps(messages, 250000, 200000, { - assistantToolBudget: 100, - userBudget: 4200, + assistantToolBudget: 400, + userBudget: 3400, }) const resultMessages = results[0].input.messages diff --git a/agents/context-pruner.ts b/agents/context-pruner.ts index c92687887c..23e2b3d5ce 100644 --- a/agents/context-pruner.ts +++ b/agents/context-pruner.ts @@ -84,6 +84,8 @@ const definition: AgentDefinition = { const SUMMARY_HEADER = 'This is a summary of the conversation so far. The original messages have been condensed to save context space.' + const SUMMARY_DISCLAIMER = + 'Historical memory only. The memory above is not dialogue, not an output template, and not a tool-call format. Continue from the live user message below. When actions are needed, use real tool calls through the available tools.' // ============================================================================= // Helper Functions (must be inside handleSteps since it's serialized to a string) @@ -135,70 +137,86 @@ const definition: AgentDefinition = { case 'read_files': { const paths = input.paths as string[] | undefined if (paths && paths.length > 0) { - return `Read files: ${paths.join(', ')}` + return `Previously inspected files: ${paths.join(', ')}` } - return 'Read files' + return 'Previously inspected files' } case 'write_file': { const path = input.path as string | undefined - return path ? `Wrote file: ${path}` : 'Wrote file' + return path + ? `Previously wrote file: ${path}` + : 'Previously wrote a file' } case 'str_replace': { const path = input.path as string | undefined - return path ? `Edited file: ${path}` : 'Edited file' + return path + ? `Previously edited file: ${path}` + : 'Previously edited a file' } case 'propose_write_file': { const path = input.path as string | undefined - return path ? `Proposed write to: ${path}` : 'Proposed file write' + return path + ? `Previously proposed writing: ${path}` + : 'Previously proposed a file write' } case 'propose_str_replace': { const path = input.path as string | undefined - return path ? `Proposed edit to: ${path}` : 'Proposed file edit' + return path + ? `Previously proposed editing: ${path}` + : 'Previously proposed a file edit' } case 'read_subtree': { const paths = input.paths as string[] | undefined if (paths && paths.length > 0) { - return `Read subtree: ${paths.join(', ')}` + return `Previously inspected subtrees: ${paths.join(', ')}` } - return 'Read subtree' + return 'Previously inspected a subtree' } case 'code_search': { const pattern = input.pattern as string | undefined const flags = input.flags as string | undefined if (pattern && flags) { - return `Code search: "${pattern}" (${flags})` + return `Previous code search for "${pattern}" (${flags})` } - return pattern ? `Code search: "${pattern}"` : 'Code search' + return pattern + ? `Previous code search for "${pattern}"` + : 'Previous code search' } case 'glob': { const pattern = input.pattern as string | undefined - return pattern ? `Glob: ${pattern}` : 'Glob search' + return pattern + ? `Previous glob search for ${pattern}` + : 'Previous glob search' } case 'list_directory': { const path = input.path as string | undefined - return path ? `Listed dir: ${path}` : 'Listed directory' + return path + ? `Previously listed directory: ${path}` + : 'Previously listed a directory' } case 'find_files': { const prompt = input.prompt as string | undefined - return prompt ? `Find files: "${prompt}"` : 'Find files' + return prompt + ? `Previous file-finding request: "${prompt}"` + : 'Previous file-finding request' } case 'run_terminal_command': { const command = input.command as string | undefined if (command) { const shortCmd = command.length > 50 ? command.slice(0, 50) + '...' : command - return `Ran command: ${shortCmd}` + return `Previously ran command: ${shortCmd}` } - return 'Ran terminal command' + return 'Previously ran a terminal command' } case 'spawn_agents': case 'spawn_agent_inline': { const agents = input.agents as | Array<{ - agent_type: string - prompt?: string - params?: Record - }> + agent_type: string + prompt?: string + params?: Record + }> | undefined const agentType = input.agent_type as string | undefined const prompt = input.prompt as string | undefined @@ -230,7 +248,7 @@ const definition: AgentDefinition = { } return detail }) - return `Spawned agents:\n${agentDetails.map((d) => `- ${d}`).join('\n')}` + return `Previously delegated agents:\n${agentDetails.map((d) => `- ${d}`).join('\n')}` } if (agentType) { const extras: string[] = [] @@ -248,11 +266,11 @@ const definition: AgentDefinition = { extras.push(`params: ${truncatedParams}`) } if (extras.length > 0) { - return `Spawned agent: ${agentType} (${extras.join(', ')})` + return `Previously delegated agent ${agentType} (${extras.join(', ')})` } - return `Spawned agent: ${agentType}` + return `Previously delegated agent ${agentType}` } - return 'Spawned agent(s)' + return 'Previously delegated agent work' } case 'write_todos': { const todos = input.todos as @@ -289,30 +307,36 @@ const definition: AgentDefinition = { return 'Suggested followups' case 'web_search': { const query = input.query as string | undefined - return query ? `Web search: "${query}"` : 'Web search' + return query + ? `Previous web search for "${query}"` + : 'Previous web search' } case 'gravity_index': { const query = input.query as string | undefined const action = input.action as string | undefined if (query) { - return `Gravity Index ${action ?? 'search'}: "${query}"` + return `Previous Gravity Index ${action ?? 'search'} for "${query}"` } - return action ? `Gravity Index ${action}` : 'Gravity Index' + return action + ? `Previous Gravity Index ${action}` + : 'Previous Gravity Index use' } case 'read_docs': { const libraryTitle = input.libraryTitle as string | undefined const topic = input.topic as string | undefined if (libraryTitle && topic) { - return `Read docs: ${libraryTitle} - ${topic}` + return `Previously consulted docs: ${libraryTitle} - ${topic}` } - return libraryTitle ? `Read docs: ${libraryTitle}` : 'Read docs' + return libraryTitle + ? `Previously consulted docs: ${libraryTitle}` + : 'Previously consulted docs' } case 'set_output': - return 'Set output' + return 'Previously set structured output' case 'set_messages': - return 'Set messages' + return 'Previously updated message history' default: - return `Used tool: ${toolName}` + return `Previously used tool ${toolName}` } } @@ -377,7 +401,11 @@ const definition: AgentDefinition = { // - Prune when context exceeds max, OR // - Prune when prompt cache will miss (>5 min gap) to take advantage of fresh context // If not, return messages with just the subagent-specific tags removed - if (agentState.contextTokenCount + TOKEN_COUNT_FUDGE_FACTOR <= maxContextLength && !cacheWillMiss) { + if ( + agentState.contextTokenCount + TOKEN_COUNT_FUDGE_FACTOR <= + maxContextLength && + !cacheWillMiss + ) { yield { toolName: 'set_messages', input: { messages: currentMessages }, @@ -404,7 +432,8 @@ const definition: AgentDefinition = { // 2. Walk backwards through summarized parts to apply token budgets // 3. Older summarized parts beyond the budgets are dropped - const assistantToolBudget: number = params?.assistantToolBudget ?? ASSISTANT_TOOL_BUDGET + const assistantToolBudget: number = + params?.assistantToolBudget ?? ASSISTANT_TOOL_BUDGET const userBudget: number = params?.userBudget ?? USER_BUDGET function shouldExcludeMessage(message: Message): boolean { @@ -429,6 +458,12 @@ const definition: AgentDefinition = { if (content.startsWith(SUMMARY_HEADER)) { content = content.slice(SUMMARY_HEADER.length).trim() } + const memoryMatch = content.match( + /([\s\S]*?)<\/historical_memory>/, + ) + if (memoryMatch) { + content = memoryMatch[1].trim() + } return content } @@ -449,7 +484,10 @@ const definition: AgentDefinition = { const trimmed = chunk.trim() const isUser = trimmed.startsWith('[USER]\n') || - trimmed.startsWith('[USER] [with image') + trimmed.startsWith('[USER] [with image') || + trimmed.startsWith('User request') || + trimmed.startsWith('User message') || + trimmed.startsWith('Current unresolved user request') return { role: isUser ? ('user' as const) : ('assistant_tool' as const), parts: [trimmed], @@ -465,10 +503,37 @@ const definition: AgentDefinition = { } } - // Filter out excluded and conversation summary messages for summarization - const messagesToSummarize = currentMessages.filter( - (message) => !shouldExcludeMessage(message) && !isConversationSummary(message), + // If pruning happens before the assistant has started responding to the + // current user prompt, preserve that prompt as a real message after the + // memory artifact. If pruning happens mid-turn, keep the prompt in the + // historical memory with the assistant/tool progress that followed it and + // append a synthetic continuation prompt instead. + const latestLiveUserPromptIndex = currentMessages.findLastIndex((message) => + message.tags?.includes('USER_PROMPT'), ) + const latestLiveUserPromptMessage = + latestLiveUserPromptIndex !== -1 + ? currentMessages[latestLiveUserPromptIndex] + : null + const isMidTurnPrune = + latestLiveUserPromptIndex !== -1 && + currentMessages + .slice(latestLiveUserPromptIndex + 1) + .some( + (message) => + !shouldExcludeMessage(message) && !isConversationSummary(message), + ) + + // Filter out excluded, conversation summary, and live-prompt messages for summarization + const messagesToSummarize = currentMessages + .filter( + (_message, index) => + isMidTurnPrune || index !== latestLiveUserPromptIndex, + ) + .filter( + (message) => + !shouldExcludeMessage(message) && !isConversationSummary(message), + ) // Find the last user message with images to preserve in the final output let lastUserImageParts: Array> = [] @@ -487,7 +552,10 @@ const definition: AgentDefinition = { } // Phase 1: Summarize ALL messages into tagged entries - const summarizedEntries: Array<{ role: 'user' | 'assistant_tool'; parts: string[] }> = [] + const summarizedEntries: Array<{ + role: 'user' | 'assistant_tool' + parts: string[] + }> = [] for (const message of messagesToSummarize) { if (message.role === 'user') { @@ -501,10 +569,10 @@ const definition: AgentDefinition = { part.type === 'image' || part.type === 'media', ) } - const imageNote = hasImages ? ' [with image(s)]' : '' + const imageNote = hasImages ? ' [image(s) were attached]' : '' summarizedEntries.push({ role: 'user', - parts: [`[USER]${imageNote}\n${text}`], + parts: [`User request${imageNote}:\n${text}`], }) } } else if (message.role === 'assistant') { @@ -531,17 +599,20 @@ const definition: AgentDefinition = { const parts: string[] = [] if (textParts.length > 0) { let combinedText = textParts.join('\n') - combinedText = truncateLongText(combinedText, ASSISTANT_MESSAGE_LIMIT * CHARS_PER_TOKEN) - parts.push(combinedText) + combinedText = truncateLongText( + combinedText, + ASSISTANT_MESSAGE_LIMIT * CHARS_PER_TOKEN, + ) + parts.push(`Progress note:\n${combinedText}`) } if (toolSummaries.length > 0) { - parts.push(toolSummaries.join('; ')) + parts.push(`Prior action record:\n${toolSummaries.join('\n')}`) } if (parts.length > 0) { summarizedEntries.push({ role: 'assistant_tool', - parts: [`[ASSISTANT]\n${parts.join('\n')}`], + parts, }) } } else if (message.role === 'tool') { @@ -559,7 +630,7 @@ const definition: AgentDefinition = { errorText = errorText.slice(0, 100) + '...' } entryParts.push( - `[TOOL ERROR: ${toolMessage.toolName}] ${errorText}`, + `Tool error from ${toolMessage.toolName}: ${errorText}`, ) } @@ -569,20 +640,20 @@ const definition: AgentDefinition = { ) { const exitCode = value.exitCode as number if (exitCode !== 0) { - entryParts.push(`[COMMAND FAILED] Exit code: ${exitCode}`) + entryParts.push(`Command failed with exit code: ${exitCode}`) } } if (toolMessage.toolName === 'ask_user') { if (value.skipped) { - entryParts.push('[USER SKIPPED QUESTION]') + entryParts.push('User skipped question') } else if ('answers' in value) { const answers = value.answers as | Array<{ - selectedOption?: string - selectedOptions?: string[] - otherText?: string - }> + selectedOption?: string + selectedOptions?: string[] + otherText?: string + }> | undefined if (answers && answers.length > 0) { const answerTexts = answers @@ -598,7 +669,7 @@ const definition: AgentDefinition = { answerTexts.length > 10_000 ? answerTexts.slice(0, 10_000) + '...' : answerTexts - entryParts.push(`[USER ANSWERED] ${truncated}`) + entryParts.push(`User answered: ${truncated}`) } } } @@ -615,7 +686,7 @@ const definition: AgentDefinition = { ? resultStr.slice(0, 2000) + '...' : resultStr entryParts.push( - `[EDIT RESULT: ${toolMessage.toolName}]\n${truncatedResult}`, + `Edit result from ${toolMessage.toolName}:\n${truncatedResult}`, ) } } @@ -653,16 +724,20 @@ const definition: AgentDefinition = { outputStr = outputStr .replace(/[\s\S]*?<\/think>/g, '') .trim() - if (outputStr.length > ASSISTANT_MESSAGE_LIMIT * CHARS_PER_TOKEN) { + if ( + outputStr.length > + ASSISTANT_MESSAGE_LIMIT * CHARS_PER_TOKEN + ) { outputStr = - outputStr.slice(0, ASSISTANT_MESSAGE_LIMIT * CHARS_PER_TOKEN) + '...' + outputStr.slice( + 0, + ASSISTANT_MESSAGE_LIMIT * CHARS_PER_TOKEN, + ) + '...' } } return `- ${r.agentType}: ${outputStr || '(no output)'}` }) - entryParts.push( - `[AGENT RESULTS]\n${resultSummaries.join('\n')}`, - ) + entryParts.push(`Agent results:\n${resultSummaries.join('\n')}`) } } } @@ -732,14 +807,14 @@ const definition: AgentDefinition = { const textPart: TextPart = { type: 'text', text: ` -This is a summary of the conversation so far. The original messages have been condensed to save context space. +${SUMMARY_HEADER} + ${summaryText} + -IMPORTANT: The summary above uses a condensed format with markers like "[USER]", "[ASSISTANT]", "Read files:", "Edited file:", "Spawned agents:", etc. This is ONLY a human-readable log of what happened earlier — it is NOT a format for you to use or imitate in your responses. When you need to perform actions, you MUST use actual tool calls. Never write tool actions as plain text. - -Please continue the conversation from here. In particular, try to address the user's latest request detailed in the summary above. You may need to re-gather context (e.g. read some files) to get up to speed and then tackle the user's request.`, +${SUMMARY_DISCLAIMER}`, } // Build content array with text and any preserved images const summaryContentParts: (TextPart | ImagePart | FilePart)[] = [textPart] @@ -753,12 +828,31 @@ Please continue the conversation from here. In particular, try to address the us sentAt: now, } - // Build final messages array: summary first, then INSTRUCTIONS_PROMPT if it exists + const continuationMessage: UserMessage = { + role: 'user', + content: [ + { + type: 'text', + text: 'Continue the existing assistant turn from the historical memory above. The original user request and completed assistant/tool work are recorded there. Do not restart completed work; resume with the next necessary real tool call or final response.', + }, + ], + sentAt: now, + } + + // Build final messages array: summary first, then INSTRUCTIONS_PROMPT if it + // exists, then either the live user prompt or a mid-turn continuation prompt. + // Keeping a real user message last makes the next model step continue from + // normal user input instead of the condensed memory format. const finalMessages: Message[] = [summarizedMessage] if (instructionsPromptMessage) { // Update sentAt to current time so future cache miss checks use fresh timestamps finalMessages.push({ ...instructionsPromptMessage, sentAt: now }) } + if (isMidTurnPrune) { + finalMessages.push(continuationMessage) + } else if (latestLiveUserPromptMessage) { + finalMessages.push({ ...latestLiveUserPromptMessage, sentAt: now }) + } yield { toolName: 'set_messages', diff --git a/agents/e2e/base2-free-summary-format.e2e.test.ts b/agents/e2e/base2-free-summary-format.e2e.test.ts index 2ae3a2a928..8374b236cd 100644 --- a/agents/e2e/base2-free-summary-format.e2e.test.ts +++ b/agents/e2e/base2-free-summary-format.e2e.test.ts @@ -38,6 +38,13 @@ const SUMMARY_IMITATION_PATTERNS = [ /^Used tool:\s/m, /^\[ASSISTANT\]\n/m, /^\[USER\]\n/m, + /^User request(?:\s|\[|:)/m, + /^Progress note:\s/m, + /^Prior action record:\s/m, + /^Previously inspected files:\s/m, + /^Previously edited file:\s/m, + /^Previously delegated agents:\s*\n/m, + /^Edit result from \w+:/m, ] /** @@ -59,8 +66,8 @@ function detectSummaryImitation(text: string): string[] { /** * Creates a pre-summarized conversation that mimics what the context pruner produces. - * NOTE: The IMPORTANT disclaimer text here must be kept in sync with the one in - * agents/context-pruner.ts. If you change the disclaimer there, update it here too. + * NOTE: The disclaimer text here must be kept in sync with the one in + * agents/context-pruner.ts. If you change the memory artifact format there, update it here too. */ function createSummarizedConversation(): Message { return { @@ -71,44 +78,50 @@ function createSummarizedConversation(): Message { text: ` This is a summary of the conversation so far. The original messages have been condensed to save context space. -[USER] + +User request: The user asked to set up a new TypeScript project with a simple utility file at src/utils.ts containing a helper function called formatDate. --- -[ASSISTANT] +Progress note: Sure, I'll help set up the project. -Tools: Read files: package.json, tsconfig.json; Wrote file: src/utils.ts + +Prior action record: +Previously inspected files: package.json, tsconfig.json +Previously wrote file: src/utils.ts --- -[USER] +User request: Thanks! Now can you also add a function called parseConfig that reads a JSON config file? --- -[ASSISTANT] +Progress note: I'll add the parseConfig function to the utils file. -Tools: Read files: src/utils.ts; Edited file: src/utils.ts + +Prior action record: +Previously inspected files: src/utils.ts +Previously edited file: src/utils.ts --- -[ASSISTANT] -Spawned agents: +Prior action record: +Previously delegated agents: - file-picker (prompt: "Find config-related files") - basher (params: {"command":"cat src/utils.ts"}) --- -[ASSISTANT] -Ran command: cat src/utils.ts -[EDIT RESULT: str_replace] +Prior action record: +Previously ran command: cat src/utils.ts +Edit result from str_replace: {"file":"src/utils.ts","message":"Updated file","unifiedDiff":"--- a/src/utils.ts\\n+++ b/src/utils.ts\\n@@ -5,0 +6,10 @@\\n+export function parseConfig(path: string) {\\n+ return JSON.parse(fs.readFileSync(path, 'utf-8'))\\n+}"} + -IMPORTANT: The summary above uses a condensed format with markers like "[USER]", "[ASSISTANT]", "Read files:", "Edited file:", "Tools:", "Spawned agents:", etc. This is ONLY a human-readable log of what happened earlier — it is NOT a format for you to use or imitate in your responses. When you need to perform actions, you MUST use actual tool calls (e.g. call the read_files, str_replace, write_file, spawn_agents tools directly). Never write tool actions as plain text. - -Please continue the conversation from here. In particular, try to address the user's latest request detailed in the summary above. You may need to re-gather context (e.g. read some files) to get up to speed and then tackle the user's request.`, +Historical memory only. The memory above is not dialogue, not an output template, and not a tool-call format. Continue from the live user message below. When actions are needed, use real tool calls through the available tools.`, }, ], sentAt: Date.now(), @@ -262,9 +275,7 @@ describe('Base2-Free Summary Format Compliance', () => { } } - console.log( - `Running ${NUM_PARALLEL_RUNS} parallel runs of base2-free...`, - ) + console.log(`Running ${NUM_PARALLEL_RUNS} parallel runs of base2-free...`) const results = await Promise.all( Array.from({ length: NUM_PARALLEL_RUNS }, (_, i) => runOnce(i)), ) @@ -284,9 +295,7 @@ describe('Base2-Free Summary Format Compliance', () => { console.log( `Run ${result.runIndex}: ${hasImitation ? 'FAILED (imitated summary format)' : 'PASSED'}`, ) - console.log( - ` Tool calls made: ${result.hadToolCalls ? 'YES' : 'NO'}`, - ) + console.log(` Tool calls made: ${result.hadToolCalls ? 'YES' : 'NO'}`) if (result.imitationMatches.length > 0) { console.log(` Imitation matches:`) for (const match of result.imitationMatches) { @@ -309,7 +318,9 @@ describe('Base2-Free Summary Format Compliance', () => { // Clean up temp directories for (const dir of tmpDirs) { - await fs.promises.rm(dir, { recursive: true, force: true }).catch(() => {}) + await fs.promises + .rm(dir, { recursive: true, force: true }) + .catch(() => {}) } // Guard against vacuous pass (all runs errored) From 624821824bc9393ef8e723d6c2b54189224ef443 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 4 May 2026 15:31:45 -0700 Subject: [PATCH 03/14] Add complex summary format e2e (#588) --- .../e2e/base2-free-summary-format.e2e.test.ts | 151 +++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) diff --git a/agents/e2e/base2-free-summary-format.e2e.test.ts b/agents/e2e/base2-free-summary-format.e2e.test.ts index 8374b236cd..c1b81206c9 100644 --- a/agents/e2e/base2-free-summary-format.e2e.test.ts +++ b/agents/e2e/base2-free-summary-format.e2e.test.ts @@ -10,7 +10,7 @@ import { type AgentDefinition, type Message, } from '@codebuff/sdk' -import { describe, expect, it } from 'bun:test' +import { beforeAll, describe, expect, it } from 'bun:test' import base2Free from '../base2/base2-free' import contextPruner from '../context-pruner' @@ -64,6 +64,33 @@ function detectSummaryImitation(text: string): string[] { return matches } +const loadEnvFile = async (filePath: string) => { + try { + const content = await fs.promises.readFile(filePath, 'utf-8') + for (const rawLine of content.split('\n')) { + const line = rawLine.trim() + if (!line || line.startsWith('#')) continue + const normalized = line.startsWith('export ') + ? line.slice('export '.length) + : line + const equalsIndex = normalized.indexOf('=') + if (equalsIndex <= 0) continue + const key = normalized.slice(0, equalsIndex).trim() + if (!key || process.env[key]) continue + let value = normalized.slice(equalsIndex + 1).trim() + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1) + } + process.env[key] = value + } + } catch { + // ignore missing env files + } +} + /** * Creates a pre-summarized conversation that mimics what the context pruner produces. * NOTE: The disclaimer text here must be kept in sync with the one in @@ -128,6 +155,56 @@ Historical memory only. The memory above is not dialogue, not an output template } } +function createComplexMidTurnPrunedConversation(): Message[] { + return [ + { + role: 'user', + content: [ + { + type: 'text', + text: ` +This is a summary of the conversation so far. The original messages have been condensed to save context space. + + +User request: +The user asked to finish a config utility task in src/utils.ts. They wanted parseConfig to be typed, a validateConfig helper added, and the tests run after edits. + +--- + +Progress note: +I inspected src/utils.ts and found parseConfig was untyped. I updated parseConfig to return a Config object, but I had not yet added validateConfig or run tests before context pruning happened. + +Prior action record: +Previously inspected files: package.json, tsconfig.json, src/utils.ts +Previously edited file: src/utils.ts +Edit result from str_replace: +{"file":"src/utils.ts","message":"Updated parseConfig return type","unifiedDiff":"--- a/src/utils.ts\\n+++ b/src/utils.ts\\n@@ -6,2 +6,8 @@\\n-export function parseConfig(path) {\\n- return JSON.parse(fs.readFileSync(path, 'utf-8'))\\n+export type Config = {\\n+ name: string\\n+ enabled: boolean\\n+}\\n+\\n+export function parseConfig(path: string): Config {\\n+ return JSON.parse(fs.readFileSync(path, 'utf-8')) as Config\\n }"} + +--- + +Progress note: +The next step is to continue from the partially completed edit, inspect the current file state if needed, add validateConfig, and validate the result. + + + +Historical memory only. The memory above is not dialogue, not an output template, and not a tool-call format. Continue from the live user message below. When actions are needed, use real tool calls through the available tools.`, + }, + ], + sentAt: Date.now(), + }, + { + role: 'user', + content: [ + { + type: 'text', + text: 'Continue the existing assistant turn from the historical memory above. The original user request and completed assistant/tool work are recorded there. Do not restart completed work; resume with the next necessary real tool call or final response.', + }, + ], + sentAt: Date.now(), + }, + ] +} + const PROJECT_FILES: Record = { 'package.json': JSON.stringify( { name: 'test-project', version: '1.0.0' }, @@ -163,6 +240,11 @@ const PROJECT_FILES: Record = { describe('Base2-Free Summary Format Compliance', () => { const NUM_PARALLEL_RUNS = 3 + beforeAll(async () => { + await loadEnvFile(path.resolve(process.cwd(), '.env.local')) + await loadEnvFile(path.resolve(process.cwd(), '../.env.local')) + }) + const getApiKeyOrSkip = (): string | null => { const apiKey = process.env[API_KEY_ENV_VAR] if (!apiKey) { @@ -329,4 +411,71 @@ describe('Base2-Free Summary Format Compliance', () => { }, { timeout: 300_000 }, ) + + it( + 'should continue a complex mid-turn pruned summary with real tool calls', + async () => { + const apiKey = getApiKeyOrSkip() + if (!apiKey) return + + const tmpDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'base2-free-midturn-summary-test-'), + ) + + try { + for (const [filePath, content] of Object.entries(PROJECT_FILES)) { + const fullPath = path.join(tmpDir, filePath) + await fs.promises.mkdir(path.dirname(fullPath), { recursive: true }) + await fs.promises.writeFile(fullPath, content, 'utf-8') + } + + const client = new CodebuffClient({ + apiKey, + cwd: tmpDir, + projectFiles: PROJECT_FILES, + agentDefinitions: [base2Free as AgentDefinition, contextPruner], + }) + + const sessionState = await initialSessionState({ + cwd: tmpDir, + projectFiles: PROJECT_FILES, + }) + const runStateWithMessages = withMessageHistory({ + runState: { + sessionState, + output: { type: 'error', message: '' }, + }, + messages: createComplexMidTurnPrunedConversation(), + }) + + const events: PrintModeEvent[] = [] + const run = await client.run({ + agent: base2Free.id, + prompt: '', + previousRun: runStateWithMessages, + maxAgentSteps: 6, + handleEvent: (event) => { + events.push(event) + }, + }) + + if (run.output.type === 'error') { + throw new Error(run.output.message) + } + + const textOutput = events + .filter((e) => e.type === 'text') + .map((e) => (e as { type: 'text'; text: string }).text) + .join('') + const hadToolCalls = events.some((e) => e.type === 'tool_call') + const imitationMatches = detectSummaryImitation(textOutput) + + expect(hadToolCalls).toBe(true) + expect(imitationMatches).toEqual([]) + } finally { + await fs.promises.rm(tmpDir, { recursive: true, force: true }) + } + }, + { timeout: 300_000 }, + ) }) From c3718ea8eaef5393a2c50f3047b54b78ae42f63c Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 4 May 2026 15:59:28 -0700 Subject: [PATCH 04/14] Tweak context summary format --- agents/__tests__/context-pruner.test.ts | 47 +++++------ agents/context-pruner.ts | 83 +++++++++---------- .../e2e/base2-free-summary-format.e2e.test.ts | 51 ++++++++---- 3 files changed, 97 insertions(+), 84 deletions(-) diff --git a/agents/__tests__/context-pruner.test.ts b/agents/__tests__/context-pruner.test.ts index 4837740e79..25b9a4707a 100644 --- a/agents/__tests__/context-pruner.test.ts +++ b/agents/__tests__/context-pruner.test.ts @@ -294,9 +294,8 @@ describe('context-pruner handleSteps', () => { // Should use a memory artifact format, not transcript role markers expect(content).toContain('') - expect(content).toContain('User request:') + expect(content).toContain('[USER]') expect(content).toContain('Progress note:') - expect(content).not.toContain('[USER]') expect(content).not.toContain('[ASSISTANT]') }) @@ -321,8 +320,8 @@ describe('context-pruner handleSteps', () => { const content = results[0].input.messages[0].content[0].text // Should contain tool summaries - expect(content).toContain('Previously inspected files: file1.ts, file2.ts') - expect(content).toContain('Previously edited file: file1.ts') + expect(content).toContain('inspected files: file1.ts, file2.ts') + expect(content).toContain('edited file: file1.ts') }) test('summarizes various tool types correctly', () => { @@ -350,10 +349,10 @@ describe('context-pruner handleSteps', () => { const results = runHandleSteps(messages, 50000, 10000) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Previously wrote file: new-file.ts') - expect(content).toContain('Previously ran command: npm test') - expect(content).toContain('Previous code search for "function"') - expect(content).toContain('Previously delegated agents:') + expect(content).toContain('wrote file: new-file.ts') + expect(content).toContain('ran command: npm test') + expect(content).toContain('code search for "function"') + expect(content).toContain('delegated agents:') expect(content).toContain('- file-picker') expect(content).toContain('- commander') }) @@ -387,7 +386,7 @@ describe('context-pruner handleSteps', () => { const results = runHandleSteps(messages, 50000, 10000) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('User request [image(s) were attached]:') + expect(content).toContain('[USER] [image(s) were attached]') }) test('removes only INSTRUCTIONS_PROMPT and SUBAGENT_SPAWN when under context limit', () => { @@ -569,7 +568,7 @@ describe('context-pruner handleSteps', () => { .text expect(summaryContent).toContain('PLEASE FIX THE BUG') expect(summaryContent).toContain('I found the likely issue.') - expect(summaryContent).toContain('Previously inspected files: src/bug.ts') + expect(summaryContent).toContain('inspected files: src/bug.ts') expect(resultMessages[1].role).toBe('user') expect(resultMessages[1].tags).toBeUndefined() @@ -653,7 +652,7 @@ describe('context-pruner handleSteps', () => { const results = runHandleSteps(messages, 50000, 10000) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Previously delegated agent file-picker') + expect(content).toContain('delegated agent file-picker') }) test('handles long terminal commands by truncating', () => { @@ -672,7 +671,7 @@ describe('context-pruner handleSteps', () => { // Should truncate to 50 chars + ... expect(content).toContain( - 'Previously ran command: npm run build -- --config=production --verbose --o...', + 'ran command: npm run build -- --config=production --verbose --o...', ) }) @@ -686,7 +685,7 @@ describe('context-pruner handleSteps', () => { const results = runHandleSteps(messages, 50000, 10000) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Previously used tool unknown_tool_name') + expect(content).toContain('used tool unknown_tool_name') }) test('handles multiple tool calls in single assistant message', () => { @@ -719,8 +718,8 @@ describe('context-pruner handleSteps', () => { const content = results[0].input.messages[0].content[0].text // Both tool calls should be in the summary - expect(content).toContain('Previously inspected files: a.ts') - expect(content).toContain('Previously inspected files: b.ts') + expect(content).toContain('inspected files: a.ts') + expect(content).toContain('inspected files: b.ts') }) test('handles mixed text and tool calls in assistant message', () => { @@ -748,7 +747,7 @@ describe('context-pruner handleSteps', () => { // Should have both text and tool summary expect(content).toContain('Let me read that file for you') - expect(content).toContain('Previously inspected files: test.ts') + expect(content).toContain('inspected files: test.ts') }) }) @@ -893,7 +892,7 @@ describe('context-pruner code_search with flags', () => { const content = results[0].input.messages[0].content[0].text expect(content).toContain( - 'Previous code search for "myFunction" (-g *.ts -i)', + 'code search for "myFunction" (-g *.ts -i)', ) }) }) @@ -1775,7 +1774,7 @@ describe('context-pruner str_replace and write_file tool results', () => { const content = results[0].input.messages[0].content[0].text // Should have both the tool call summary and the full result - expect(content).toContain('Previously edited file: src/file.ts') + expect(content).toContain('edited file: src/file.ts') expect(content).toContain('Edit result from str_replace:') expect(content).toContain('errorMessage') expect(content).toContain('No match found for old string') @@ -1826,7 +1825,7 @@ describe('context-pruner glob and list_directory tools', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Previous glob search for **/*.ts') + expect(content).toContain('glob search for **/*.ts') }) test('summarizes list_directory tool with path', () => { @@ -1841,7 +1840,7 @@ describe('context-pruner glob and list_directory tools', () => { const results = runHandleSteps(messages) const content = results[0].input.messages[0].content[0].text - expect(content).toContain('Previously listed directory: src') + expect(content).toContain('listed directory: src') }) test('summarizes read_subtree tool with paths', () => { @@ -1857,7 +1856,7 @@ describe('context-pruner glob and list_directory tools', () => { const content = results[0].input.messages[0].content[0].text expect(content).toContain( - 'Previously inspected subtrees: src/components, src/utils', + 'inspected subtrees: src/components, src/utils', ) }) }) @@ -2357,10 +2356,10 @@ describe('context-pruner dual-budget behavior', () => { // === Tool call summaries present === expect(content).toContain( - 'Previously inspected files: src/model.ts, src/service.ts', + 'inspected files: src/model.ts, src/service.ts', ) - expect(content).toContain('Previously edited file: src/model.ts') - expect(content).toContain('Previously delegated agents:') + expect(content).toContain('edited file: src/model.ts') + expect(content).toContain('delegated agents:') // === str_replace result: present but truncated at 2k chars === expect(content).toContain('Edit result from str_replace:') diff --git a/agents/context-pruner.ts b/agents/context-pruner.ts index 23e2b3d5ce..f60b569d9a 100644 --- a/agents/context-pruner.ts +++ b/agents/context-pruner.ts @@ -137,77 +137,73 @@ const definition: AgentDefinition = { case 'read_files': { const paths = input.paths as string[] | undefined if (paths && paths.length > 0) { - return `Previously inspected files: ${paths.join(', ')}` + return `inspected files: ${paths.join(', ')}` } - return 'Previously inspected files' + return 'inspected files' } case 'write_file': { const path = input.path as string | undefined - return path - ? `Previously wrote file: ${path}` - : 'Previously wrote a file' + return path ? `wrote file: ${path}` : 'wrote a file' } case 'str_replace': { const path = input.path as string | undefined - return path - ? `Previously edited file: ${path}` - : 'Previously edited a file' + return path ? `edited file: ${path}` : 'edited a file' } case 'propose_write_file': { const path = input.path as string | undefined return path - ? `Previously proposed writing: ${path}` - : 'Previously proposed a file write' + ? `proposed writing: ${path}` + : 'proposed a file write' } case 'propose_str_replace': { const path = input.path as string | undefined return path - ? `Previously proposed editing: ${path}` - : 'Previously proposed a file edit' + ? `proposed editing: ${path}` + : 'proposed a file edit' } case 'read_subtree': { const paths = input.paths as string[] | undefined if (paths && paths.length > 0) { - return `Previously inspected subtrees: ${paths.join(', ')}` + return `inspected subtrees: ${paths.join(', ')}` } - return 'Previously inspected a subtree' + return 'inspected a subtree' } case 'code_search': { const pattern = input.pattern as string | undefined const flags = input.flags as string | undefined if (pattern && flags) { - return `Previous code search for "${pattern}" (${flags})` + return `code search for "${pattern}" (${flags})` } return pattern - ? `Previous code search for "${pattern}"` - : 'Previous code search' + ? `code search for "${pattern}"` + : 'code search' } case 'glob': { const pattern = input.pattern as string | undefined return pattern - ? `Previous glob search for ${pattern}` - : 'Previous glob search' + ? `glob search for ${pattern}` + : 'glob search' } case 'list_directory': { const path = input.path as string | undefined return path - ? `Previously listed directory: ${path}` - : 'Previously listed a directory' + ? `listed directory: ${path}` + : 'listed a directory' } case 'find_files': { const prompt = input.prompt as string | undefined return prompt - ? `Previous file-finding request: "${prompt}"` - : 'Previous file-finding request' + ? `file-finding request: "${prompt}"` + : 'file-finding request' } case 'run_terminal_command': { const command = input.command as string | undefined if (command) { const shortCmd = command.length > 50 ? command.slice(0, 50) + '...' : command - return `Previously ran command: ${shortCmd}` + return `ran command: ${shortCmd}` } - return 'Previously ran a terminal command' + return 'ran a terminal command' } case 'spawn_agents': case 'spawn_agent_inline': { @@ -248,7 +244,7 @@ const definition: AgentDefinition = { } return detail }) - return `Previously delegated agents:\n${agentDetails.map((d) => `- ${d}`).join('\n')}` + return `delegated agents:\n${agentDetails.map((d) => `- ${d}`).join('\n')}` } if (agentType) { const extras: string[] = [] @@ -266,11 +262,11 @@ const definition: AgentDefinition = { extras.push(`params: ${truncatedParams}`) } if (extras.length > 0) { - return `Previously delegated agent ${agentType} (${extras.join(', ')})` + return `delegated agent ${agentType} (${extras.join(', ')})` } - return `Previously delegated agent ${agentType}` + return `delegated agent ${agentType}` } - return 'Previously delegated agent work' + return 'delegated agent work' } case 'write_todos': { const todos = input.todos as @@ -308,35 +304,35 @@ const definition: AgentDefinition = { case 'web_search': { const query = input.query as string | undefined return query - ? `Previous web search for "${query}"` - : 'Previous web search' + ? `web search for "${query}"` + : 'web search' } case 'gravity_index': { const query = input.query as string | undefined const action = input.action as string | undefined if (query) { - return `Previous Gravity Index ${action ?? 'search'} for "${query}"` + return `Gravity Index ${action ?? 'search'} for "${query}"` } return action - ? `Previous Gravity Index ${action}` - : 'Previous Gravity Index use' + ? `Gravity Index ${action}` + : 'Gravity Index use' } case 'read_docs': { const libraryTitle = input.libraryTitle as string | undefined const topic = input.topic as string | undefined if (libraryTitle && topic) { - return `Previously consulted docs: ${libraryTitle} - ${topic}` + return `consulted docs: ${libraryTitle} - ${topic}` } return libraryTitle - ? `Previously consulted docs: ${libraryTitle}` - : 'Previously consulted docs' + ? `consulted docs: ${libraryTitle}` + : 'consulted docs' } case 'set_output': - return 'Previously set structured output' + return 'set structured output' case 'set_messages': - return 'Previously updated message history' + return 'updated message history' default: - return `Previously used tool ${toolName}` + return `used tool ${toolName}` } } @@ -483,8 +479,7 @@ const definition: AgentDefinition = { return chunks.map((chunk) => { const trimmed = chunk.trim() const isUser = - trimmed.startsWith('[USER]\n') || - trimmed.startsWith('[USER] [with image') || + trimmed.startsWith('[USER]') || trimmed.startsWith('User request') || trimmed.startsWith('User message') || trimmed.startsWith('Current unresolved user request') @@ -572,7 +567,7 @@ const definition: AgentDefinition = { const imageNote = hasImages ? ' [image(s) were attached]' : '' summarizedEntries.push({ role: 'user', - parts: [`User request${imageNote}:\n${text}`], + parts: [`[USER]${imageNote}\n${text}`], }) } } else if (message.role === 'assistant') { @@ -606,7 +601,7 @@ const definition: AgentDefinition = { parts.push(`Progress note:\n${combinedText}`) } if (toolSummaries.length > 0) { - parts.push(`Prior action record:\n${toolSummaries.join('\n')}`) + parts.push(toolSummaries.join('\n')) } if (parts.length > 0) { diff --git a/agents/e2e/base2-free-summary-format.e2e.test.ts b/agents/e2e/base2-free-summary-format.e2e.test.ts index c1b81206c9..51df280b89 100644 --- a/agents/e2e/base2-free-summary-format.e2e.test.ts +++ b/agents/e2e/base2-free-summary-format.e2e.test.ts @@ -22,8 +22,28 @@ import type { PrintModeEvent } from '@codebuff/common/types/print-mode' * instead of using actual tool calls via the API. * * These patterns come from the context pruner's summarizeToolCall function. + * Both the current format (lowercase bare verbs, [USER] role tag) and + * historical formats are matched as defensive checks. */ const SUMMARY_IMITATION_PATTERNS = [ + // Current format (new bare-verb style) + /^\[USER\](?:\s|\[|$)/m, + /^\[ASSISTANT\]\n/m, + /^Progress note:\s/m, + /^inspected files?:\s/m, + /^inspected subtrees?:\s/m, + /^wrote file:\s/m, + /^edited file:\s/m, + /^proposed writing:\s/m, + /^proposed editing:\s/m, + /^listed directory:\s/m, + /^code search for\s/m, + /^glob search for\s/m, + /^ran command:\s/m, + /^delegated agents?:\s*\n/m, + /^delegated agent\s/m, + /^Edit result from \w+:/m, + // Older format (kept as defensive checks) /^Read files?:\s/m, /^Edited file:\s/m, /^Wrote file:\s/m, @@ -36,15 +56,11 @@ const SUMMARY_IMITATION_PATTERNS = [ /^Listed dir:\s/m, /^Read subtree:\s/m, /^Used tool:\s/m, - /^\[ASSISTANT\]\n/m, - /^\[USER\]\n/m, /^User request(?:\s|\[|:)/m, - /^Progress note:\s/m, /^Prior action record:\s/m, /^Previously inspected files:\s/m, /^Previously edited file:\s/m, /^Previously delegated agents:\s*\n/m, - /^Edit result from \w+:/m, ] /** @@ -106,7 +122,7 @@ function createSummarizedConversation(): Message { This is a summary of the conversation so far. The original messages have been condensed to save context space. -User request: +[USER] The user asked to set up a new TypeScript project with a simple utility file at src/utils.ts containing a helper function called formatDate. --- @@ -114,13 +130,14 @@ The user asked to set up a new TypeScript project with a simple utility file at Progress note: Sure, I'll help set up the project. -Prior action record: -Previously inspected files: package.json, tsconfig.json -Previously wrote file: src/utils.ts +--- + +inspected files: package.json, tsconfig.json +wrote file: src/utils.ts --- -User request: +[USER] Thanks! Now can you also add a function called parseConfig that reads a JSON config file? --- @@ -128,21 +145,23 @@ Thanks! Now can you also add a function called parseConfig that reads a JSON con Progress note: I'll add the parseConfig function to the utils file. -Prior action record: -Previously inspected files: src/utils.ts -Previously edited file: src/utils.ts +--- + +inspected files: src/utils.ts +edited file: src/utils.ts --- -Prior action record: -Previously delegated agents: +delegated agents: - file-picker (prompt: "Find config-related files") - basher (params: {"command":"cat src/utils.ts"}) --- -Prior action record: -Previously ran command: cat src/utils.ts +ran command: cat src/utils.ts + +--- + Edit result from str_replace: {"file":"src/utils.ts","message":"Updated file","unifiedDiff":"--- a/src/utils.ts\\n+++ b/src/utils.ts\\n@@ -5,0 +6,10 @@\\n+export function parseConfig(path: string) {\\n+ return JSON.parse(fs.readFileSync(path, 'utf-8'))\\n+}"} From 16fd4bcc2bfa329e6ffe1cb43f63993b21a0bfb8 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 4 May 2026 16:08:19 -0700 Subject: [PATCH 05/14] Remove redundant 'Free session' text from status bar --- cli/src/components/status-bar.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/src/components/status-bar.tsx b/cli/src/components/status-bar.tsx index 9657f5f14d..4216a1d666 100644 --- a/cli/src/components/status-bar.tsx +++ b/cli/src/components/status-bar.tsx @@ -181,9 +181,7 @@ export const StatusBar = ({ ? getFreebuffModel(freebuffSession.model).displayName : null return ( - - {modelName ? `${modelName} · ` : ''}Free session ·{' '} - {formatSessionRemaining(sessionProgress.remainingMs)} + {modelName ? `${modelName} · ` : ''}{formatSessionRemaining(sessionProgress.remainingMs)} ) } From 658a3adf348fd32da5b767635da5e8d5cc0d36f9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 May 2026 23:09:41 +0000 Subject: [PATCH 06/14] Bump Freebuff version to 0.0.76 --- freebuff/cli/release/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freebuff/cli/release/package.json b/freebuff/cli/release/package.json index ab30e36991..0d9a450127 100644 --- a/freebuff/cli/release/package.json +++ b/freebuff/cli/release/package.json @@ -1,6 +1,6 @@ { "name": "freebuff", - "version": "0.0.75", + "version": "0.0.76", "description": "The world's strongest free coding agent", "license": "MIT", "bin": { From efcb10b7a9968c6a3bd71d9d23ed49df82668f38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 May 2026 23:10:24 +0000 Subject: [PATCH 07/14] Bump version to 1.0.668 --- cli/release/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/release/package.json b/cli/release/package.json index 4e79b581f0..91a60ce72c 100644 --- a/cli/release/package.json +++ b/cli/release/package.json @@ -1,6 +1,6 @@ { "name": "codebuff", - "version": "1.0.667", + "version": "1.0.668", "description": "AI coding agent", "license": "MIT", "bin": { From 9dde8dd12610578d05b61930e45918591427718a Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 4 May 2026 16:38:06 -0700 Subject: [PATCH 08/14] Fix for showing thinekr output --- .../__tests__/sdk-event-handlers.test.ts | 138 ++++++++++++++++++ cli/src/utils/sdk-event-handlers.ts | 15 +- 2 files changed, 149 insertions(+), 4 deletions(-) diff --git a/cli/src/utils/__tests__/sdk-event-handlers.test.ts b/cli/src/utils/__tests__/sdk-event-handlers.test.ts index ce88ad0f2d..8f34427b1d 100644 --- a/cli/src/utils/__tests__/sdk-event-handlers.test.ts +++ b/cli/src/utils/__tests__/sdk-event-handlers.test.ts @@ -251,4 +251,142 @@ describe('sdk-event-handlers', () => { }) expect(getStreamingAgents().size).toBe(0) }) + + test('handles spawn_agents tool results for agents with tool blocks (lastMessage mode)', () => { + const { ctx, getMessages, getStreamingAgents } = createTestContext() + + // Create an agent block with an existing tool block (simulating thinker agent's read_files) + ctx.message.updater.updateAiMessageBlocks(() => [ + { + type: 'agent', + agentId: 'tool-1-0', + agentName: 'Thinker', + agentType: 'thinker-with-files-gemini', + content: '', + status: 'running', + blocks: [ + { + type: 'tool', + toolCallId: 'read-1', + toolName: 'read_files', + input: { paths: ['package.json'] }, + output: 'package contents', + }, + ], + initialPrompt: 'Think about this', + spawnToolCallId: 'tool-1', + spawnIndex: 0, + } as any, + ]) + ctx.streaming.setStreamingAgents(() => new Set(['tool-1-0'])) + + const handleEvent = createEventHandler(ctx) + const toolResultEvent: ToolResultEvent = { + type: 'tool_result', + toolCallId: 'tool-1', + toolName: 'spawn_agents', + output: [ + { + type: 'json', + value: [ + { + agentName: 'thinker-with-files-gemini', + value: { + type: 'lastMessage', + value: [ + { + role: 'assistant', + content: [ + { type: 'text', text: 'Here is the analysis result.' }, + ], + }, + ], + }, + }, + ], + }, + ], + } + handleEvent(toolResultEvent) + + const agentBlock = (getMessages()[0].blocks ?? [])[0] as AgentContentBlock + expect(agentBlock.status).toBe('complete') + // Should have the tool block AND the final text content + expect(agentBlock.blocks).toHaveLength(2) + expect(agentBlock.blocks?.[0]).toMatchObject({ + type: 'tool', + toolName: 'read_files', + }) + expect(agentBlock.blocks?.[1]).toMatchObject({ + type: 'text', + content: 'Here is the analysis result.', + }) + expect(getStreamingAgents().size).toBe(0) + }) + + test('preserves streamed text content and skips duplicate final content', () => { + const { ctx, getMessages, getStreamingAgents } = createTestContext() + + // Create an agent block with existing text blocks (simulating streamed output like basher) + ctx.message.updater.updateAiMessageBlocks(() => [ + { + type: 'agent', + agentId: 'tool-1-0', + agentName: 'Basher', + agentType: 'basher', + content: '', + status: 'running', + blocks: [ + { + type: 'text', + content: 'Streamed output from basher', + textType: 'text', + }, + ], + initialPrompt: 'Run a command', + spawnToolCallId: 'tool-1', + spawnIndex: 0, + } as any, + ]) + ctx.streaming.setStreamingAgents(() => new Set(['tool-1-0'])) + + const handleEvent = createEventHandler(ctx) + const toolResultEvent: ToolResultEvent = { + type: 'tool_result', + toolCallId: 'tool-1', + toolName: 'spawn_agents', + output: [ + { + type: 'json', + value: [ + { + agentName: 'basher', + value: { + type: 'lastMessage', + value: [ + { + role: 'assistant', + content: [ + { type: 'text', text: 'Streamed output from basher' }, + ], + }, + ], + }, + }, + ], + }, + ], + } + handleEvent(toolResultEvent) + + const agentBlock = (getMessages()[0].blocks ?? [])[0] as AgentContentBlock + expect(agentBlock.status).toBe('complete') + // Should NOT duplicate the streamed text — only the original text block + expect(agentBlock.blocks).toHaveLength(1) + expect(agentBlock.blocks?.[0]).toMatchObject({ + type: 'text', + content: 'Streamed output from basher', + }) + expect(getStreamingAgents().size).toBe(0) + }) }) diff --git a/cli/src/utils/sdk-event-handlers.ts b/cli/src/utils/sdk-event-handlers.ts index 6f3b94649d..6f304f147e 100644 --- a/cli/src/utils/sdk-event-handlers.ts +++ b/cli/src/utils/sdk-event-handlers.ts @@ -371,12 +371,19 @@ const updateSpawnAgentBlocks = ( if (result?.value) { const { content, hasError } = extractSpawnAgentResultContent(result.value) - // Preserve streamed content (agents like basher stream their output) - const hasStreamedContent = block.blocks.length > 0 - if (hasError || content || hasStreamedContent) { + // Check if the agent already streamed text content (e.g., basher). + // Agents like thinker return all output at the end via lastMessage, + // so we should add final content even if they have tool blocks. + const hasStreamedTextContent = block.blocks.some( + (b) => b.type === 'text' && b.textType === 'text' + ) + const finalBlocks = content && !hasStreamedTextContent + ? [...block.blocks, { type: 'text', content } as ContentBlock] + : block.blocks + if (hasError || finalBlocks.length > 0) { return { ...block, - blocks: hasStreamedContent ? block.blocks : [{ type: 'text', content } as ContentBlock], + blocks: finalBlocks, status: hasError ? ('failed' as const) : ('complete' as const), } } From 91c1378014b338e9cafc372ad208db165e425f4c Mon Sep 17 00:00:00 2001 From: brandon chen <9735006+brandonkachen@users.noreply.github.com> Date: Mon, 4 May 2026 16:53:26 -0700 Subject: [PATCH 09/14] [codex] overhaul Freebuff premium sessions (#589) Co-authored-by: James Grugett --- .../components/freebuff-model-selector.tsx | 89 +- cli/src/components/status-bar.tsx | 42 +- cli/src/components/waiting-room-screen.tsx | 28 +- common/src/constants/freebuff-models.ts | 18 +- common/src/types/freebuff-session.ts | 40 +- .../migrations/0050_overrated_stellaris.sql | 1 + .../src/db/migrations/meta/0050_snapshot.json | 3198 +++++++++++++++++ .../src/db/migrations/meta/_journal.json | 7 + packages/internal/src/db/schema.ts | 10 +- .../session/__tests__/session.test.ts | 4 +- .../free-session/__tests__/public-api.test.ts | 322 +- web/src/server/free-session/public-api.ts | 180 +- web/src/server/free-session/store.ts | 112 +- 13 files changed, 3787 insertions(+), 264 deletions(-) create mode 100644 packages/internal/src/db/migrations/0050_overrated_stellaris.sql create mode 100644 packages/internal/src/db/migrations/meta/0050_snapshot.json diff --git a/cli/src/components/freebuff-model-selector.tsx b/cli/src/components/freebuff-model-selector.tsx index 3a67ffed8f..c3111b2770 100644 --- a/cli/src/components/freebuff-model-selector.tsx +++ b/cli/src/components/freebuff-model-selector.tsx @@ -7,8 +7,10 @@ import { DEFAULT_FREEBUFF_MODEL_ID, FALLBACK_FREEBUFF_MODEL_ID, FREEBUFF_MODELS, + FREEBUFF_PREMIUM_SESSION_LIMIT, getFreebuffDeploymentAvailabilityLabel, isFreebuffModelAvailable, + isFreebuffPremiumModelId, } from '@codebuff/common/constants/freebuff-models' import { joinFreebuffQueue } from '../hooks/use-freebuff-session' @@ -31,6 +33,10 @@ const FREEBUFF_MODEL_SELECTOR_MODELS: readonly FreebuffModelOption[] = [ ...FREEBUFF_MODELS.filter((model) => model.id !== DEFAULT_FREEBUFF_MODEL_ID), ] +function formatSessionUnits(units: number): string { + return Number.isInteger(units) ? String(units) : units.toFixed(1) +} + /** * Dual-purpose model picker: * - Pre-chat landing (session 'none'): user hasn't joined any queue. Picking @@ -45,11 +51,6 @@ const FREEBUFF_MODEL_SELECTOR_MODELS: readonly FreebuffModelOption[] = [ * Always stacked vertically. On narrow terminals where the longest one-line * label wouldn't fit, the secondary details (warning / deployment hours) * spill onto an indented second line under the name. - * - * No queue-position hint: traffic doesn't reach the threshold where a wait - * would form, so showing "N in line" everywhere just adds noise (and width). - * The picker still surfaces "Closed" (outside deployment hours) and "Limit - * used" (per-user quota) inline since those gate the actual click. */ export const FreebuffModelSelector: React.FC = () => { const theme = useTheme() @@ -91,15 +92,30 @@ export const FreebuffModelSelector: React.FC = () => { } }, [now, selectedModel, session, setSelectedModel]) + const committedModelId = session?.status === 'queued' ? session.model : null + const rateLimitsByModel = + session && 'rateLimitsByModel' in session + ? session.rateLimitsByModel + : undefined + + const getQuotaHint = useCallback( + (modelId: string): string => { + const rateLimit = rateLimitsByModel?.[modelId] + if (rateLimit) { + return `${formatSessionUnits(rateLimit.recentCount)}/${rateLimit.limit} used` + } + return isFreebuffPremiumModelId(modelId) + ? `0/${FREEBUFF_PREMIUM_SESSION_LIMIT} used` + : 'Unlimited' + }, + [rateLimitsByModel], + ) + const BUTTON_CHROME = 4 // 2 border + 2 padding // Decide whether secondary details (warning / deployment hours) get their - // own indented line under the name. Trigger: the widest one-line button - // wouldn't fit in our content budget. All buttons share a uniform width so - // the column reads as a clean stack of equal choices. We size to the - // *label* — Closed / Limit used hints can transiently push the text past - // this width, but they're rare (deployment hours closing, daily quota hit) - // and a small one-time grow is fine. + // own indented line under the name. All buttons share a uniform width so + // the column reads as a clean stack of equal choices. const { wrapDetails, buttonOuterWidth } = useMemo(() => { const detailsTextLen = (model: FreebuffModelOption): number => { const parts: number[] = [] @@ -108,9 +124,14 @@ export const FreebuffModelSelector: React.FC = () => { } if (model.warning) parts.push(model.warning.length) if (parts.length === 0) return 0 - return parts.reduce((a, b) => a + b, 0) + (parts.length - 1) * 3 /* " · " */ + return ( + parts.reduce((a, b) => a + b, 0) + (parts.length - 1) * 3 + ) /* " · " */ } + const hintLen = (model: FreebuffModelOption): number => + Math.max(getQuotaHint(model.id).length, 'Closed'.length) + const oneLineLen = (model: FreebuffModelOption): number => { const inlineDetails = detailsTextLen(model) return ( @@ -118,12 +139,19 @@ export const FreebuffModelSelector: React.FC = () => { model.displayName.length + 3 /* " · " */ + model.tagline.length + - (inlineDetails > 0 ? 3 + inlineDetails : 0) + (inlineDetails > 0 ? 3 + inlineDetails : 0) + + 1 /* space before hint */ + + hintLen(model) ) } const labelLineLen = (model: FreebuffModelOption): number => - 2 + model.displayName.length + 3 + model.tagline.length + 2 + + model.displayName.length + + 3 + + model.tagline.length + + 1 + + hintLen(model) const detailsLineLen = (model: FreebuffModelOption): number => { const len = detailsTextLen(model) @@ -148,16 +176,8 @@ export const FreebuffModelSelector: React.FC = () => { contentMaxWidth, ), } - }, [contentMaxWidth, deploymentAvailabilityLabel]) + }, [contentMaxWidth, deploymentAvailabilityLabel, getQuotaHint]) - // "Already committed to this model" — only when the server has us queued - // on it. On the landing screen (status 'none'), nothing is committed yet, - // so picking the focused model is always a real action (first join). - const committedModelId = session?.status === 'queued' ? session.model : null - const rateLimitsByModel = - session && 'rateLimitsByModel' in session - ? session.rateLimitsByModel - : undefined const isJoinable = useCallback( (modelId: string) => { if (!isFreebuffModelAvailable(modelId, new Date(now))) return false @@ -230,19 +250,13 @@ export const FreebuffModelSelector: React.FC = () => { const isHovered = hoveredId === model.id const isFocused = focusedId === model.id const isAvailable = isFreebuffModelAvailable(model.id, new Date(now)) - const rateLimit = rateLimitsByModel?.[model.id] - const isQuotaExhausted = - rateLimit !== undefined && rateLimit.recentCount >= rateLimit.limit - const canJoin = isAvailable && !isQuotaExhausted + const canJoin = isJoinable(model.id) // Clickable whenever picking would actually do something — i.e. // anything except re-picking the queue we're already in. const interactable = !pending && canJoin && model.id !== committedModelId - const hint = !isAvailable - ? 'Closed' - : isQuotaExhausted - ? 'Limit used' - : '' + const quotaHint = getQuotaHint(model.id) + const hint = isAvailable ? quotaHint : 'Closed' // Focused row: green border + arrow indicator + bold name. The name // itself stays the normal foreground color so it doesn't shout — the @@ -251,7 +265,7 @@ export const FreebuffModelSelector: React.FC = () => { const fgColor = canJoin ? theme.foreground : theme.muted const mutedColor = theme.muted const warningColor = theme.secondary - const hintColor = theme.secondary + const hintColor = canJoin ? theme.muted : theme.secondary const borderColor = isFocused ? theme.primary @@ -303,16 +317,17 @@ export const FreebuffModelSelector: React.FC = () => { {showInlineWarning && ( · {model.warning} )} - {hint && {hint}} + {hint} {showWrappedDetails && ( - + {model.availability === 'deployment_hours' && ( {deploymentAvailabilityLabel} )} - {model.availability === 'deployment_hours' && - model.warning && · } + {model.availability === 'deployment_hours' && model.warning && ( + · + )} {model.warning && ( {model.warning} )} diff --git a/cli/src/components/status-bar.tsx b/cli/src/components/status-bar.tsx index 4216a1d666..82c2b16d8f 100644 --- a/cli/src/components/status-bar.tsx +++ b/cli/src/components/status-bar.tsx @@ -66,6 +66,9 @@ const formatSessionRemaining = (ms: number): string => { return minutes === 0 ? `${hours}h left` : `${hours}h ${minutes}m left` } +const formatSessionUnits = (units: number): string => + Number.isInteger(units) ? String(units) : units.toFixed(1) + interface StatusBarProps { timerStartTime: number | null isAtBottom: boolean @@ -131,7 +134,8 @@ export const StatusBar = ({ case 'clipboard': // Use green color for feedback success messages - const isFeedbackSuccess = statusIndicatorState.message.includes('Feedback sent') + const isFeedbackSuccess = + statusIndicatorState.message.includes('Feedback sent') return ( {statusIndicatorState.message} @@ -142,12 +146,7 @@ export const StatusBar = ({ return Reconnected case 'retrying': - return ( - - ) + return case 'connecting': return @@ -180,8 +179,17 @@ export const StatusBar = ({ freebuffSession?.status === 'active' ? getFreebuffModel(freebuffSession.model).displayName : null + const quotaText = + freebuffSession?.status === 'active' && freebuffSession.rateLimit + ? `Premium ${formatSessionUnits(freebuffSession.rateLimit.recentCount)}/${freebuffSession.rateLimit.limit} used · ` + : freebuffSession?.status === 'active' + ? 'Unlimited · ' + : '' return ( - {modelName ? `${modelName} · ` : ''}{formatSessionRemaining(sessionProgress.remainingMs)} + + {modelName ? `${modelName} · ` : ''} + {quotaText}Free session ·{' '} + {formatSessionRemaining(sessionProgress.remainingMs)} ) } @@ -258,12 +266,18 @@ export const StatusBar = ({ }} > {elapsedTimeContent} - {onStop && (statusIndicatorState.kind === 'waiting' || statusIndicatorState.kind === 'streaming') && ( - ■ Esc - )} - {onEndSession && statusIndicatorState.kind === 'idle' && freebuffSession?.status === 'active' && ( - ✕ End session - )} + {onStop && + (statusIndicatorState.kind === 'waiting' || + statusIndicatorState.kind === 'streaming') && ( + ■ Esc + )} + {onEndSession && + statusIndicatorState.kind === 'idle' && + freebuffSession?.status === 'active' && ( + + ✕ End session + + )} {sessionProgress !== null && sessionProgress.remainingMs < COUNTDOWN_VISIBLE_MS && statusIndicatorState.kind !== 'idle' && ( diff --git a/cli/src/components/waiting-room-screen.tsx b/cli/src/components/waiting-room-screen.tsx index a87980905a..36de9a86d0 100644 --- a/cli/src/components/waiting-room-screen.tsx +++ b/cli/src/components/waiting-room-screen.tsx @@ -3,10 +3,7 @@ import { useRenderer } from '@opentui/react' import React, { useMemo, useState } from 'react' import { Button } from './button' -import { - ChoiceAdBanner, - CHOICE_AD_BANNER_HEIGHT, -} from './choice-ad-banner' +import { ChoiceAdBanner, CHOICE_AD_BANNER_HEIGHT } from './choice-ad-banner' import { FreebuffModelSelector } from './freebuff-model-selector' import { ShimmerText } from './shimmer-text' import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit' @@ -59,6 +56,9 @@ const formatRetryAfter = (ms: number): string => { return rem === 0 ? `${hours}h` : `${hours}h ${rem}m` } +const formatSessionUnits = (units: number): string => + Number.isInteger(units) ? String(units) : units.toFixed(1) + const PRIVACY_SIGNAL_LABELS: Partial> = { anonymous: 'anonymized network', @@ -263,17 +263,16 @@ export const WaitingRoomScreen: React.FC = ({ Elapsed {formatElapsed(elapsedMs)} - {/* Per-model session quota (e.g. DeepSeek V4 Pro caps at 5/12h). - Only rendered for rate-limited models so the Minimax queue - stays clutter-free. */} + {/* Premium session quota. Minimax is unlimited, so it has no + rateLimit payload and skips this line. */} {session.rateLimit && ( - Sessions + Premium sessions - {session.rateLimit.recentCount} /{' '} + {formatSessionUnits(session.rateLimit.recentCount)} /{' '} {session.rateLimit.limit} - used in last {session.rateLimit.windowHours}h + used in the last 20 hours )} @@ -346,8 +345,8 @@ export const WaitingRoomScreen: React.FC = ({ )} - {/* Per-model session quota exhausted (e.g. 5+ DeepSeek sessions in - the last 12h). Terminal for this run — the user can exit and come + {/* Shared premium-session quota exhausted. Terminal for this run — + the user can exit and come back once the oldest session in the window rolls off. */} {session?.status === 'rate_limited' && ( <> @@ -357,10 +356,9 @@ export const WaitingRoomScreen: React.FC = ({ You've used{' '} - {session.recentCount} of {session.limit} + {formatSessionUnits(session.recentCount)} of {session.limit} {' '} - hour-long sessions on {session.model} in the last{' '} - {session.windowHours}h. Try again in{' '} + premium sessions in the last 20 hours. Try again in{' '} {formatRetryAfter(session.retryAfterMs)} diff --git a/common/src/constants/freebuff-models.ts b/common/src/constants/freebuff-models.ts index 657d5343db..3f96183287 100644 --- a/common/src/constants/freebuff-models.ts +++ b/common/src/constants/freebuff-models.ts @@ -30,6 +30,8 @@ export const FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID = 'deepseek/deepseek-v4-pro' export const FREEBUFF_GLM_MODEL_ID = 'z-ai/glm-5.1' export const FREEBUFF_KIMI_MODEL_ID = 'moonshotai/kimi-k2.6' export const FREEBUFF_MINIMAX_MODEL_ID = 'minimax/minimax-m2.7' +export const FREEBUFF_PREMIUM_SESSION_LIMIT = 5 +export const FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS = 20 const FREEBUFF_EASTERN_TIMEZONE = 'America/New_York' const FREEBUFF_PACIFIC_TIMEZONE = 'America/Los_Angeles' @@ -78,7 +80,7 @@ export const FREEBUFF_MODELS = [ { id: FREEBUFF_MINIMAX_MODEL_ID, displayName: 'MiniMax M2.7', - tagline: 'Fastest', + tagline: 'Fastest, unlimited', availability: 'always', }, ] as const satisfies readonly FreebuffModelOption[] @@ -92,6 +94,12 @@ export const LEGACY_FREEBUFF_MODELS = [ }, ] as const satisfies readonly FreebuffModelOption[] +export const FREEBUFF_PREMIUM_MODEL_IDS = [ + FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, + FREEBUFF_KIMI_MODEL_ID, + FREEBUFF_GLM_MODEL_ID, +] as const + export const SUPPORTED_FREEBUFF_MODELS = [ ...FREEBUFF_MODELS, ...LEGACY_FREEBUFF_MODELS, @@ -100,6 +108,7 @@ export const SUPPORTED_FREEBUFF_MODELS = [ export type FreebuffModelId = (typeof FREEBUFF_MODELS)[number]['id'] export type SupportedFreebuffModelId = (typeof SUPPORTED_FREEBUFF_MODELS)[number]['id'] +export type FreebuffPremiumModelId = (typeof FREEBUFF_PREMIUM_MODEL_IDS)[number] /** What new freebuff users see selected in the picker. DeepSeek is the * smartest of the free options; the picker surfaces its data-collection @@ -136,6 +145,13 @@ export function isSupportedFreebuffModelId( return SUPPORTED_FREEBUFF_MODELS.some((m) => m.id === id) } +export function isFreebuffPremiumModelId( + id: string | null | undefined, +): id is FreebuffPremiumModelId { + if (!id) return false + return FREEBUFF_PREMIUM_MODEL_IDS.some((modelId) => modelId === id) +} + export function resolveSupportedFreebuffModel( id: string | null | undefined, ): SupportedFreebuffModelId { diff --git a/common/src/types/freebuff-session.ts b/common/src/types/freebuff-session.ts index b80ffed26a..6f44d202bd 100644 --- a/common/src/types/freebuff-session.ts +++ b/common/src/types/freebuff-session.ts @@ -7,11 +7,12 @@ */ /** - * Per-model usage counter surfaced to the CLI so the waiting-room UI can - * render "N of M sessions used" alongside queue/active state. Present when - * the joined model has a rate limit applied. `recentCount` is the number of - * admissions inside `windowHours` at the time the response was produced — - * see also the standalone `rate_limited` status for the reject path. + * Usage counter surfaced to the CLI so the waiting-room UI can render + * "N of M sessions used" alongside queue/active state. Present when the + * joined model consumes premium Freebuff sessions. `recentCount` is the + * rounded session units inside `windowHours` at the time the response was + * produced — see also the standalone `rate_limited` status for the reject + * path. */ export interface FreebuffSessionRateLimit { model: string @@ -61,9 +62,9 @@ export type FreebuffSessionServerResponse = * Present on GET responses; not returned from POST (POST never * produces `none`). */ queueDepthByModel?: Record - /** Current quota snapshots for rate-limited models, keyed by model id. - * Lets the picker show exhausted daily/session caps before the user - * commits to a queue. */ + /** Current quota snapshots for premium models, keyed by model id. Lets + * the picker show rolling premium-session usage before the user commits + * to a queue. */ rateLimitsByModel?: FreebuffSessionRateLimitByModel } | { @@ -81,9 +82,7 @@ export type FreebuffSessionServerResponse = queueDepthByModel: Record estimatedWaitMs: number queuedAt: string - /** Rate-limit quota for rate-limited models. Absent - * for unlimited models or when the status was produced outside the - * rate-limit check path (e.g. pure read via GET). */ + /** Premium-session quota for this model. Absent for unlimited models. */ rateLimit?: FreebuffSessionRateLimit rateLimitsByModel?: FreebuffSessionRateLimitByModel } @@ -95,9 +94,7 @@ export type FreebuffSessionServerResponse = admittedAt: string expiresAt: string remainingMs: number - /** Rate-limit quota for rate-limited models. Absent - * for unlimited models or when the status was produced outside the - * rate-limit check path (e.g. pure read via GET). */ + /** Premium-session quota for this model. Absent for unlimited models. */ rateLimit?: FreebuffSessionRateLimit rateLimitsByModel?: FreebuffSessionRateLimitByModel } @@ -162,21 +159,20 @@ export type FreebuffSessionServerResponse = status: 'banned' } | { - /** User has used up their per-model admission quota in the rolling - * window. Returned from POST - * /session before the user is placed in the queue. `retryAfterMs` is - * the time until the oldest admission inside the window falls off - * and one quota slot opens up — clients should show the user when - * they can try again. Terminal for the CLI's current poll session; + /** User has used up their shared premium-session quota in the rolling + * window. Returned from POST /session before the user is placed in the + * queue. `retryAfterMs` is the time until enough session units fall out + * of the window to open one quota slot — clients should show the user + * when they can try again. Terminal for the CLI's current poll session; * the user can exit and come back later. */ status: 'rate_limited' /** The freebuff model the user tried to join. */ model: string - /** Max admissions permitted per window (e.g. 5). */ + /** Max premium session units permitted per window (e.g. 5). */ limit: number /** Rolling window size in hours (e.g. 20). */ windowHours: number - /** Admission count inside the window at check time — will be ≥ limit. */ + /** Premium session units inside the window at check time — will be ≥ limit. */ recentCount: number /** Milliseconds from now until the oldest admission in the window * exits and the user regains one quota slot. */ diff --git a/packages/internal/src/db/migrations/0050_overrated_stellaris.sql b/packages/internal/src/db/migrations/0050_overrated_stellaris.sql new file mode 100644 index 0000000000..9255e390bc --- /dev/null +++ b/packages/internal/src/db/migrations/0050_overrated_stellaris.sql @@ -0,0 +1 @@ +ALTER TABLE "free_session_admit" ADD COLUMN "session_units" numeric(3, 1) DEFAULT '1.0' NOT NULL; \ No newline at end of file diff --git a/packages/internal/src/db/migrations/meta/0050_snapshot.json b/packages/internal/src/db/migrations/meta/0050_snapshot.json new file mode 100644 index 0000000000..7e56edc6e1 --- /dev/null +++ b/packages/internal/src/db/migrations/meta/0050_snapshot.json @@ -0,0 +1,3198 @@ +{ + "id": "4c7aa6ac-8afc-4c2c-b0a4-2bbfcde731b8", + "prevId": "927c6e1e-457f-4815-99d1-96701792e9e5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": ["provider", "providerAccountId"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ad_impression": { + "name": "ad_impression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gravity'" + }, + "ad_text": { + "name": "ad_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta": { + "name": "cta", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_url": { + "name": "click_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imp_url": { + "name": "imp_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extra_pixels": { + "name": "extra_pixels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "payout": { + "name": "payout", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "credits_granted": { + "name": "credits_granted", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_operation_id": { + "name": "grant_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "served_at": { + "name": "served_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "impression_fired_at": { + "name": "impression_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clicked_at": { + "name": "clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_ad_impression_user": { + "name": "idx_ad_impression_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ad_impression_imp_url": { + "name": "idx_ad_impression_imp_url", + "columns": [ + { + "expression": "imp_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ad_impression_user_id_user_id_fk": { + "name": "ad_impression_user_id_user_id_fk", + "tableFrom": "ad_impression", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ad_impression_imp_url_unique": { + "name": "ad_impression_imp_url_unique", + "nullsNotDistinct": false, + "columns": ["imp_url"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config": { + "name": "agent_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 1) AS INTEGER)", + "type": "stored" + } + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 2) AS INTEGER)", + "type": "stored" + } + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 3) AS INTEGER)", + "type": "stored" + } + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_config_publisher": { + "name": "idx_agent_config_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_publisher_id_publisher_id_fk": { + "name": "agent_config_publisher_id_publisher_id_fk", + "tableFrom": "agent_config", + "tableTo": "publisher", + "columnsFrom": ["publisher_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_config_publisher_id_id_version_pk": { + "name": "agent_config_publisher_id_id_version_pk", + "columns": ["publisher_id", "id", "version"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run": { + "name": "agent_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '/', 1)\n ELSE NULL\n END", + "type": "stored" + } + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(split_part(agent_id, '/', 2), '@', 1)\n ELSE agent_id\n END", + "type": "stored" + } + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '@', 2)\n ELSE NULL\n END", + "type": "stored" + } + }, + "ancestor_run_ids": { + "name": "ancestor_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "root_run_id": { + "name": "root_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[1] ELSE id END", + "type": "stored" + } + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END", + "type": "stored" + } + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)", + "type": "stored" + } + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "total_steps": { + "name": "total_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "direct_credits": { + "name": "direct_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_credits": { + "name": "total_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_run_user_id": { + "name": "idx_agent_run_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_parent": { + "name": "idx_agent_run_parent", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_root": { + "name": "idx_agent_run_root", + "columns": [ + { + "expression": "root_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_agent_id": { + "name": "idx_agent_run_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_publisher": { + "name": "idx_agent_run_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_status": { + "name": "idx_agent_run_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_ancestors_gin": { + "name": "idx_agent_run_ancestors_gin", + "columns": [ + { + "expression": "ancestor_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_agent_run_completed_publisher_agent": { + "name": "idx_agent_run_completed_publisher_agent", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_recent": { + "name": "idx_agent_run_completed_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_version": { + "name": "idx_agent_run_completed_version", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_user": { + "name": "idx_agent_run_completed_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_user_id_user_id_fk": { + "name": "agent_run_user_id_user_id_fk", + "tableFrom": "agent_run", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_step": { + "name": "agent_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "credits": { + "name": "credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "child_run_ids": { + "name": "child_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spawned_count": { + "name": "spawned_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "array_length(child_run_ids, 1)", + "type": "stored" + } + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "agent_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_step_number_per_run": { + "name": "unique_step_number_per_run", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_run_id": { + "name": "idx_agent_step_run_id", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_children_gin": { + "name": "idx_agent_step_children_gin", + "columns": [ + { + "expression": "child_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "agent_step_agent_run_id_agent_run_id_fk": { + "name": "agent_step_agent_run_id_agent_run_id_fk", + "tableFrom": "agent_step", + "tableTo": "agent_run", + "columnsFrom": ["agent_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_ledger": { + "name": "credit_ledger", + "schema": "", + "columns": { + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_credit_ledger_active_balance": { + "name": "idx_credit_ledger_active_balance", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "balance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_org": { + "name": "idx_credit_ledger_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_subscription": { + "name": "idx_credit_ledger_subscription", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credit_ledger_org_id_org_id_fk": { + "name": "credit_ledger_org_id_org_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_api_keys": { + "name": "encrypted_api_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "api_key_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_api_keys_user_id_user_id_fk": { + "name": "encrypted_api_keys_user_id_user_id_fk", + "tableFrom": "encrypted_api_keys", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_api_keys_user_id_type_pk": { + "name": "encrypted_api_keys_user_id_type_pk", + "columns": ["user_id", "type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session": { + "name": "free_session", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "free_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "active_instance_id": { + "name": "active_instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cf_country": { + "name": "cf_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geoip_country": { + "name": "geoip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_block_reason": { + "name": "country_block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_privacy_signals": { + "name": "ip_privacy_signals", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "client_ip_hash": { + "name": "client_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_checked_at": { + "name": "country_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_session_queue": { + "name": "idx_free_session_queue", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_free_session_expiry": { + "name": "idx_free_session_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_user_id_user_id_fk": { + "name": "free_session_user_id_user_id_fk", + "tableFrom": "free_session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session_admit": { + "name": "free_session_admit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "session_units": { + "name": "session_units", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": true, + "default": "'1.0'" + } + }, + "indexes": { + "idx_free_session_admit_user_model_time": { + "name": "idx_free_session_admit_user_model_time", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "admitted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_admit_user_id_user_id_fk": { + "name": "free_session_admit_user_id_user_id_fk", + "tableFrom": "free_session_admit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_eval_results": { + "name": "git_eval_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cost_mode": { + "name": "cost_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoner_model": { + "name": "reasoner_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_model": { + "name": "agent_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limit_override": { + "name": "limit_override", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credits_per_block": { + "name": "credits_per_block", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_duration_hours": { + "name": "block_duration_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weekly_credit_limit": { + "name": "weekly_credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "limit_override_user_id_user_id_fk": { + "name": "limit_override_user_id_user_id_fk", + "tableFrom": "limit_override", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "reasoning_text": { + "name": "reasoning_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byok": { + "name": "byok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttft_ms": { + "name": "ttft_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_user_id_idx": { + "name": "message_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_finished_at_user_id_idx": { + "name": "message_finished_at_user_id_idx", + "columns": [ + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_idx": { + "name": "message_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_finished_at_idx": { + "name": "message_org_id_finished_at_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_org_id_org_id_fk": { + "name": "message_org_id_org_id_fk", + "tableFrom": "message", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org": { + "name": "org", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_limit": { + "name": "credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_alerts": { + "name": "billing_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weekly_reports": { + "name": "weekly_reports", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_owner_id_user_id_fk": { + "name": "org_owner_id_user_id_fk", + "tableFrom": "org", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_slug_unique": { + "name": "org_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + }, + "org_stripe_customer_id_unique": { + "name": "org_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": ["stripe_customer_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_feature": { + "name": "org_feature", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_feature_active": { + "name": "idx_org_feature_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_feature_org_id_org_id_fk": { + "name": "org_feature_org_id_org_id_fk", + "tableFrom": "org_feature", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_feature_org_id_feature_pk": { + "name": "org_feature_org_id_feature_pk", + "columns": ["org_id", "feature"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invite": { + "name": "org_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_org_invite_token": { + "name": "idx_org_invite_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_email": { + "name": "idx_org_invite_email", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_expires": { + "name": "idx_org_invite_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invite_org_id_org_id_fk": { + "name": "org_invite_org_id_org_id_fk", + "tableFrom": "org_invite", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invite_invited_by_user_id_fk": { + "name": "org_invite_invited_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "org_invite_accepted_by_user_id_fk": { + "name": "org_invite_accepted_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": ["accepted_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invite_token_unique": { + "name": "org_invite_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_member": { + "name": "org_member", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_member_org_id_org_id_fk": { + "name": "org_member_org_id_org_id_fk", + "tableFrom": "org_member", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_member_user_id_user_id_fk": { + "name": "org_member_user_id_user_id_fk", + "tableFrom": "org_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_member_org_id_user_id_pk": { + "name": "org_member_org_id_user_id_pk", + "columns": ["org_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_repo": { + "name": "org_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_org_repo_active": { + "name": "idx_org_repo_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_repo_unique": { + "name": "idx_org_repo_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_repo_org_id_org_id_fk": { + "name": "org_repo_org_id_org_id_fk", + "tableFrom": "org_repo", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_repo_approved_by_user_id_fk": { + "name": "org_repo_approved_by_user_id_fk", + "tableFrom": "org_repo", + "tableTo": "user", + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publisher": { + "name": "publisher", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "publisher_user_id_user_id_fk": { + "name": "publisher_user_id_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_org_id_org_id_fk": { + "name": "publisher_org_id_org_id_fk", + "tableFrom": "publisher", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_created_by_user_id_fk": { + "name": "publisher_created_by_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "publisher_single_owner": { + "name": "publisher_single_owner", + "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_legacy": { + "name": "is_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": ["referrer_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": ["referred_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": ["referrer_id", "referred_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_auth_hash": { + "name": "cli_auth_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "session_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_cli_auth_code_idx": { + "name": "session_cli_auth_code_idx", + "columns": [ + { + "expression": "fingerprint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cli_auth_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"session\".\"cli_auth_hash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": ["fingerprint_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scheduled_tier": { + "name": "scheduled_tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_customer": { + "name": "idx_subscription_customer", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_user": { + "name": "idx_subscription_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_status": { + "name": "idx_subscription_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_user_id_user_id_fk": { + "name": "subscription_user_id_user_id_fk", + "tableFrom": "subscription", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_failure": { + "name": "sync_failure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_failure_retry": { + "name": "idx_sync_failure_retry", + "columns": [ + { + "expression": "retry_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sync_failure\".\"retry_count\" < 5", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + }, + "referral_limit": { + "name": "referral_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fallback_to_a_la_carte": { + "name": "fallback_to_a_la_carte", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": ["stripe_customer_id"] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": ["referral_code"] + }, + "user_discord_id_unique": { + "name": "user_discord_id_unique", + "nullsNotDistinct": false, + "columns": ["discord_id"] + }, + "user_handle_unique": { + "name": "user_handle_unique", + "nullsNotDistinct": false, + "columns": ["handle"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": ["identifier", "token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": ["pending", "completed"] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": ["running", "completed", "failed", "cancelled"] + }, + "public.agent_step_status": { + "name": "agent_step_status", + "schema": "public", + "values": ["running", "completed", "skipped"] + }, + "public.api_key_type": { + "name": "api_key_type", + "schema": "public", + "values": ["anthropic", "gemini", "openai"] + }, + "public.free_session_status": { + "name": "free_session_status", + "schema": "public", + "values": ["queued", "active"] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "free", + "referral", + "referral_legacy", + "subscription", + "purchase", + "admin", + "organization", + "ad" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": ["owner", "admin", "member"] + }, + "public.session_type": { + "name": "session_type", + "schema": "public", + "values": ["web", "pat", "cli"] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "incomplete", + "incomplete_expired", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "paused" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/internal/src/db/migrations/meta/_journal.json b/packages/internal/src/db/migrations/meta/_journal.json index d93bf88575..6dcc930048 100644 --- a/packages/internal/src/db/migrations/meta/_journal.json +++ b/packages/internal/src/db/migrations/meta/_journal.json @@ -351,6 +351,13 @@ "when": 1777929052630, "tag": "0049_loud_madame_masque", "breakpoints": true + }, + { + "idx": 50, + "version": "7", + "when": 1777936763321, + "tag": "0050_overrated_stellaris", + "breakpoints": true } ] } diff --git a/packages/internal/src/db/schema.ts b/packages/internal/src/db/schema.ts index 28406296d9..ee4f32509d 100644 --- a/packages/internal/src/db/schema.ts +++ b/packages/internal/src/db/schema.ts @@ -911,7 +911,9 @@ export const freeSession = pgTable( /** * Audit log of every admission — one row per queued→active transition. Used - * to rate-limit heavy users (e.g. no more than 5 DeepSeek sessions per 12h). + * to track shared premium-session usage for Freebuff's 5 sessions / 20h + * allowance. `session_units` starts at 1.0 and may be reduced when users end + * active sessions early. * * Separate from `free_session` because that table is one-row-per-user (state, * not history); the UPSERT path there would otherwise destroy prior admissions. @@ -932,6 +934,12 @@ export const freeSessionAdmit = pgTable( }) .notNull() .defaultNow(), + session_units: numeric('session_units', { + precision: 3, + scale: 1, + }) + .notNull() + .default('1.0'), }, (table) => [ // Rate-limit lookup: WHERE user_id=$1 AND model=$2 AND admitted_at > $cutoff 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 6f630e4d25..af77ac8f5c 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 @@ -112,7 +112,7 @@ function makeSessionDeps(overrides: Partial = {}): SessionDeps & { promoteQueuedUser: async () => null, // No admits in handler tests — the rate-limit check reads empty and // every request falls through to the queue. - listRecentAdmits: async () => [], + listRecentPremiumAdmits: async () => [], now: () => now, getSessionRow: async (userId) => rows.get(userId) ?? null, queueDepthsByModel: async () => { @@ -124,7 +124,7 @@ function makeSessionDeps(overrides: Partial = {}): SessionDeps & { return out }, queuePositionFor: async () => 1, - endSession: async (userId) => { + endSession: async ({ userId }) => { rows.delete(userId) }, joinOrTakeOver: async ({ userId, model, now, countryAccess }) => { 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 153021d8ee..d29c2cb1fa 100644 --- a/web/src/server/free-session/__tests__/public-api.test.ts +++ b/web/src/server/free-session/__tests__/public-api.test.ts @@ -5,6 +5,8 @@ import { FREEBUFF_GEMINI_PRO_MODEL_ID, FREEBUFF_GLM_MODEL_ID, FREEBUFF_KIMI_MODEL_ID, + FREEBUFF_PREMIUM_SESSION_LIMIT, + FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS, } from '@codebuff/common/constants/freebuff-models' import { @@ -26,6 +28,7 @@ interface AdmitRecord { user_id: string model: string admitted_at: Date + session_units?: number } function makeDeps(overrides: Partial = {}): SessionDeps & { @@ -67,17 +70,20 @@ function makeDeps(overrides: Partial = {}): SessionDeps & { } return n }, - listRecentAdmits: async ({ userId, model, since, limit }) => { + listRecentPremiumAdmits: async ({ userId, models, since }) => { return admits .filter( (a) => a.user_id === userId && - a.model === model && + models.includes(a.model) && a.admitted_at.getTime() >= since.getTime(), ) .sort((a, b) => a.admitted_at.getTime() - b.admitted_at.getTime()) - .slice(0, limit) - .map((a) => a.admitted_at) + .map((a) => ({ + admittedAt: a.admitted_at, + model: a.model, + sessionUnits: a.session_units ?? 1, + })) }, promoteQueuedUser: async ({ userId, model, sessionLengthMs, now }) => { const row = rows.get(userId) @@ -86,12 +92,38 @@ function makeDeps(overrides: Partial = {}): SessionDeps & { row.admitted_at = now row.expires_at = new Date(now.getTime() + sessionLengthMs) row.updated_at = now - admits.push({ user_id: userId, model, admitted_at: now }) + admits.push({ + user_id: userId, + model, + admitted_at: now, + session_units: 1, + }) return row }, now: () => currentNow, getSessionRow: async (userId) => rows.get(userId) ?? null, - endSession: async (userId) => { + endSession: async ({ userId, now, sessionLengthMs }) => { + const row = rows.get(userId) + if ( + row?.status === 'active' && + row.admitted_at && + row.expires_at && + row.expires_at.getTime() > now.getTime() + ) { + const latest = admits + .filter((a) => a.user_id === userId && a.model === row.model) + .sort((a, b) => b.admitted_at.getTime() - a.admitted_at.getTime())[0] + if (latest) { + const usedMs = Math.max( + 0, + Math.min( + sessionLengthMs, + now.getTime() - row.admitted_at.getTime(), + ), + ) + latest.session_units = Math.ceil((usedMs / sessionLengthMs) * 10) / 10 + } + } rows.delete(userId) }, queueDepthsByModel: async () => { @@ -239,8 +271,8 @@ describe('requestSession', () => { expect(deps.rows.get('u1')?.model).toBe(FREEBUFF_GLM_MODEL_ID) expect(state.rateLimit).toEqual({ model: FREEBUFF_GLM_MODEL_ID, - limit: 5, - windowHours: 12, + limit: FREEBUFF_PREMIUM_SESSION_LIMIT, + windowHours: FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS, recentCount: 0, }) }) @@ -269,8 +301,8 @@ describe('requestSession', () => { expect(state.instanceId).not.toBe('inst-pre') expect(state.rateLimit).toEqual({ model: FREEBUFF_GLM_MODEL_ID, - limit: 5, - windowHours: 12, + limit: FREEBUFF_PREMIUM_SESSION_LIMIT, + windowHours: FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS, recentCount: 0, }) }) @@ -282,7 +314,11 @@ describe('requestSession', () => { deps._tick(new Date(deps._now().getTime() + 1000)) await requestSession({ userId: 'u2', model: DEFAULT_MODEL, deps }) deps._tick(new Date(deps._now().getTime() + 1000)) - await requestSession({ userId: 'u3', model: 'deepseek/deepseek-v4-pro', deps }) + await requestSession({ + userId: 'u3', + model: 'deepseek/deepseek-v4-pro', + deps, + }) const state = await getSessionState({ userId: 'u1', deps }) if (state.status !== 'queued') throw new Error('unreachable') @@ -396,51 +432,101 @@ describe('requestSession', () => { expect(s3.status).toBe('active') }) - // Per-user rate limit (5 DeepSeek admissions per 18h) — the wire limit is + // Per-user premium session limit (5 units per 20h) — the wire limit is // hard-coded in public-api.ts, so tests seed the fake admit log directly - // rather than configuring it. DeepSeek runs 24/7, so the open-time anchor - // here just keeps these scenarios deterministic against the test clock. - const DEEPSEEK_MODEL = FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID - const DEEPSEEK_LIMIT = 5 - const DEEPSEEK_WINDOW_HOURS = 18 - const DEEPSEEK_OPEN_TIME = new Date('2026-04-17T16:00:00Z') - - test('rate_limited: 5th DeepSeek admit in window blocks the 6th attempt', async () => { - deps._tick(DEEPSEEK_OPEN_TIME) - // Seed 5 admits inside the 18h window, spaced so we can verify retryAfter + // rather than configuring it. + const PREMIUM_MODEL = FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID + const KIMI_MODEL = FREEBUFF_KIMI_MODEL_ID + const PREMIUM_LIMIT = FREEBUFF_PREMIUM_SESSION_LIMIT + const PREMIUM_WINDOW_HOURS = FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS + const PREMIUM_OPEN_TIME = new Date('2026-04-17T16:00:00Z') + + test('rate_limited: shared premium pool blocks the next premium session at 5 units', async () => { + deps._tick(PREMIUM_OPEN_TIME) + const now = deps._now() + for (let i = 0; i < PREMIUM_LIMIT; i++) { + deps.admits.push({ + user_id: 'u1', + model: i === 0 ? KIMI_MODEL : PREMIUM_MODEL, + admitted_at: new Date(now.getTime() - (19 - i) * 60 * 60 * 1000), + }) + } + + const state = await requestSession({ + userId: 'u1', + model: PREMIUM_MODEL, + deps, + }) + expect(state.status).toBe('rate_limited') + if (state.status !== 'rate_limited') throw new Error('unreachable') + expect(state.model).toBe(PREMIUM_MODEL) + expect(state.limit).toBe(PREMIUM_LIMIT) + expect(state.windowHours).toBe(PREMIUM_WINDOW_HOURS) + expect(state.recentCount).toBe(PREMIUM_LIMIT) + expect(state.retryAfterMs).toBe(60 * 60 * 1000) + expect(deps.rows.has('u1')).toBe(false) + }) + + test('rate_limited: DeepSeek admit outside 20h window does not count', async () => { + deps._tick(PREMIUM_OPEN_TIME) + const now = deps._now() + deps.admits.push({ + user_id: 'u1', + model: PREMIUM_MODEL, + admitted_at: new Date(now.getTime() - 21 * 60 * 60 * 1000), + }) + + const state = await requestSession({ + userId: 'u1', + model: PREMIUM_MODEL, + deps, + }) + expect(state.status).toBe('queued') + if (state.status !== 'queued') throw new Error('unreachable') + expect(state.rateLimit).toEqual({ + model: PREMIUM_MODEL, + limit: PREMIUM_LIMIT, + windowHours: PREMIUM_WINDOW_HOURS, + recentCount: 0, + }) + }) + + test('rate_limited: 5th Kimi admit in window blocks the 6th attempt', async () => { + deps._tick(PREMIUM_OPEN_TIME) + // Seed 5 admits inside the 20h window, spaced so we can verify retryAfter // points at the oldest one sliding off. const now = deps._now() - // Oldest: 17h ago (still in window). Next 4: 1h, 2h, 3h, 4h ago. - const ages = [17, 4, 3, 2, 1] + // Oldest: 19h ago (still in window). Next 4: 1h, 2h, 3h, 4h ago. + const ages = [19, 4, 3, 2, 1] for (const hoursAgo of ages) { deps.admits.push({ user_id: 'u1', - model: DEEPSEEK_MODEL, + model: KIMI_MODEL, admitted_at: new Date(now.getTime() - hoursAgo * 60 * 60 * 1000), }) } const state = await requestSession({ userId: 'u1', - model: DEEPSEEK_MODEL, + model: KIMI_MODEL, deps, }) expect(state.status).toBe('rate_limited') if (state.status !== 'rate_limited') throw new Error('unreachable') - expect(state.model).toBe(DEEPSEEK_MODEL) - expect(state.limit).toBe(DEEPSEEK_LIMIT) - expect(state.windowHours).toBe(DEEPSEEK_WINDOW_HOURS) - expect(state.recentCount).toBe(DEEPSEEK_LIMIT) - // Oldest admit is 17h ago; slot opens when it hits 18h, i.e. in 1h. + expect(state.model).toBe(KIMI_MODEL) + expect(state.limit).toBe(PREMIUM_LIMIT) + expect(state.windowHours).toBe(PREMIUM_WINDOW_HOURS) + expect(state.recentCount).toBe(PREMIUM_LIMIT) + // Oldest admit is 19h ago; slot opens when it hits 20h, i.e. in 1h. expect(state.retryAfterMs).toBe(60 * 60 * 1000) // Blocked before any row is written — the user doesn't take a queue slot. expect(deps.rows.has('u1')).toBe(false) }) - test('rate_limited: legacy GLM 5.1 keeps the deployment-hours quota', async () => { - deps._tick(DEEPSEEK_OPEN_TIME) + test('rate_limited: legacy GLM 5.1 uses the shared premium quota', async () => { + deps._tick(PREMIUM_OPEN_TIME) const now = deps._now() - for (let i = 0; i < DEEPSEEK_LIMIT; i++) { + for (let i = 0; i < PREMIUM_LIMIT; i++) { deps.admits.push({ user_id: 'u1', model: FREEBUFF_GLM_MODEL_ID, @@ -456,26 +542,26 @@ describe('requestSession', () => { expect(state.status).toBe('rate_limited') if (state.status !== 'rate_limited') throw new Error('unreachable') expect(state.model).toBe(FREEBUFF_GLM_MODEL_ID) - expect(state.limit).toBe(DEEPSEEK_LIMIT) - expect(state.windowHours).toBe(12) + expect(state.limit).toBe(PREMIUM_LIMIT) + expect(state.windowHours).toBe(PREMIUM_WINDOW_HOURS) }) - test('rate_limited: admits outside the 18h window do not count', async () => { - deps._tick(DEEPSEEK_OPEN_TIME) - // 5 admits, each just over 18h old → all fall off the window. + test('rate_limited: admits outside the 20h window do not count', async () => { + deps._tick(PREMIUM_OPEN_TIME) + // 5 admits, each just over 20h old → all fall off the window. const now = deps._now() for (let i = 0; i < 5; i++) { deps.admits.push({ user_id: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, admitted_at: new Date( - now.getTime() - (DEEPSEEK_WINDOW_HOURS * 60 * 60 * 1000 + 60_000 + i), + now.getTime() - (PREMIUM_WINDOW_HOURS * 60 * 60 * 1000 + 60_000 + i), ), }) } const state = await requestSession({ userId: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, deps, }) expect(state.status).toBe('queued') @@ -504,48 +590,76 @@ describe('requestSession', () => { }) test('queued DeepSeek response carries the current admit count', async () => { - deps._tick(DEEPSEEK_OPEN_TIME) + deps._tick(PREMIUM_OPEN_TIME) const now = deps._now() // 2 admits in the window — under the limit so the user still queues. deps.admits.push({ user_id: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, admitted_at: new Date(now.getTime() - 60 * 60 * 1000), }) deps.admits.push({ user_id: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, admitted_at: new Date(now.getTime() - 30 * 60 * 1000), }) const state = await requestSession({ userId: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, deps, }) if (state.status !== 'queued') throw new Error('unreachable') expect(state.rateLimit).toEqual({ - model: DEEPSEEK_MODEL, - limit: DEEPSEEK_LIMIT, - windowHours: DEEPSEEK_WINDOW_HOURS, + model: PREMIUM_MODEL, + limit: PREMIUM_LIMIT, + windowHours: PREMIUM_WINDOW_HOURS, recentCount: 2, }) }) - test('rate_limited: takeover of an active DeepSeek row is allowed even when at cap', async () => { - // Reclaim path: user has an active+unexpired DeepSeek session and restarts + test('rate_limited: fractional premium usage under the cap can start another session', async () => { + deps._tick(PREMIUM_OPEN_TIME) + const now = deps._now() + deps.admits.push({ + user_id: 'u1', + model: KIMI_MODEL, + admitted_at: new Date(now.getTime() - 19 * 60 * 60 * 1000), + session_units: 0.9, + }) + for (let i = 0; i < 4; i++) { + deps.admits.push({ + user_id: 'u1', + model: KIMI_MODEL, + admitted_at: new Date(now.getTime() - (i + 1) * 60 * 60 * 1000), + }) + } + + const state = await requestSession({ + userId: 'u1', + model: KIMI_MODEL, + deps, + }) + + expect(state.status).toBe('queued') + if (state.status !== 'queued') throw new Error('unreachable') + expect(state.rateLimit?.recentCount).toBe(4.9) + }) + + test('rate_limited: takeover of an active premium row is allowed even when at cap', async () => { + // Reclaim path: user has an active+unexpired premium session and restarts // the CLI. POST must rotate their instance id (takeover) and NOT reject // with rate_limited — otherwise they'd be stranded with a live session // they can't reconnect to. The 5th admission is already in the log, so // this also exercises "at the cap" rather than "over the cap". - deps._tick(DEEPSEEK_OPEN_TIME) + deps._tick(PREMIUM_OPEN_TIME) const now = deps._now() // Seed 5 prior admits (the cap), with the latest one matching the // active row we're about to install. - const ages = [11, 4, 3, 2, 0] + const ages = [19, 4, 3, 2, 0] for (const hoursAgo of ages) { deps.admits.push({ user_id: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, admitted_at: new Date(now.getTime() - hoursAgo * 60 * 60 * 1000), }) } @@ -556,7 +670,7 @@ describe('requestSession', () => { user_id: 'u1', status: 'active', active_instance_id: 'inst-pre', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, queued_at: admittedAt, admitted_at: admittedAt, expires_at: new Date(admittedAt.getTime() + SESSION_LEN), @@ -566,27 +680,27 @@ describe('requestSession', () => { const state = await requestSession({ userId: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, deps, }) expect(state.status).toBe('active') if (state.status !== 'active') throw new Error('unreachable') // Instance id rotated; quota snapshot still reflects the full window. expect(state.instanceId).not.toBe('inst-pre') - expect(state.rateLimit?.recentCount).toBe(DEEPSEEK_LIMIT) + expect(state.rateLimit?.recentCount).toBe(PREMIUM_LIMIT) }) - test('rate_limited: reclaim of a queued DeepSeek row is allowed even when at cap', async () => { + test('rate_limited: reclaim of a queued premium row is allowed even when at cap', async () => { // Same reclaim exception for queued rows: if a user has already queued // (say they slipped in just before their 5th admit landed), a subsequent // POST from the same CLI must preserve their queue position instead of // flipping to rate_limited. - deps._tick(DEEPSEEK_OPEN_TIME) + deps._tick(PREMIUM_OPEN_TIME) const now = deps._now() - for (let i = 0; i < DEEPSEEK_LIMIT; i++) { + for (let i = 0; i < PREMIUM_LIMIT; i++) { deps.admits.push({ user_id: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, admitted_at: new Date(now.getTime() - (i + 1) * 60 * 60 * 1000), }) } @@ -595,7 +709,7 @@ describe('requestSession', () => { user_id: 'u1', status: 'queued', active_instance_id: 'inst-pre', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, queued_at: queuedAt, admitted_at: null, expires_at: null, @@ -605,7 +719,7 @@ describe('requestSession', () => { const state = await requestSession({ userId: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, deps, }) expect(state.status).toBe('queued') @@ -613,20 +727,20 @@ describe('requestSession', () => { // Same position (1) since we preserved queued_at and nobody else is // ahead; the instance id rotated so any prior CLI is superseded. expect(state.instanceId).not.toBe('inst-pre') - expect(state.rateLimit?.recentCount).toBe(DEEPSEEK_LIMIT) + expect(state.rateLimit?.recentCount).toBe(PREMIUM_LIMIT) }) - test('rate_limited: expired DeepSeek row is not a reclaim — quota still applies', async () => { + test('rate_limited: expired premium row is not a reclaim — quota still applies', async () => { // The stored row's expires_at is in the past, so it doesn't represent // an in-flight session. This POST is effectively a fresh request and // must be blocked by the quota. - deps._tick(DEEPSEEK_OPEN_TIME) + deps._tick(PREMIUM_OPEN_TIME) const now = deps._now() - const ages = [11, 4, 3, 2, 1] + const ages = [19, 4, 3, 2, 1] for (const hoursAgo of ages) { deps.admits.push({ user_id: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, admitted_at: new Date(now.getTime() - hoursAgo * 60 * 60 * 1000), }) } @@ -635,7 +749,7 @@ describe('requestSession', () => { user_id: 'u1', status: 'active', active_instance_id: 'inst-pre', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, queued_at: admittedAt, admitted_at: admittedAt, expires_at: new Date(admittedAt.getTime() + SESSION_LEN), @@ -644,7 +758,7 @@ describe('requestSession', () => { }) const state = await requestSession({ userId: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, deps, }) expect(state.status).toBe('rate_limited') @@ -652,18 +766,18 @@ describe('requestSession', () => { test('instant-admit bumps the quota count for the freshly-written admit row', async () => { const admitDeps = makeDeps({ getInstantAdmitCapacity: () => 3 }) - admitDeps._tick(DEEPSEEK_OPEN_TIME) + admitDeps._tick(PREMIUM_OPEN_TIME) // 1 existing admit in the window; this new call should instant-admit and // write a second row, so the response's recentCount reflects 2. const now = admitDeps._now() admitDeps.admits.push({ user_id: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, admitted_at: new Date(now.getTime() - 30 * 60 * 1000), }) const state = await requestSession({ userId: 'u1', - model: DEEPSEEK_MODEL, + model: PREMIUM_MODEL, deps: admitDeps, }) if (state.status !== 'active') throw new Error('unreachable') @@ -697,6 +811,27 @@ describe('getSessionState', () => { expect(state).toEqual({ status: 'none', queueDepthByModel: {} }) }) + test('no row surfaces used premium quota before joining', async () => { + const now = deps._now() + deps.admits.push({ + user_id: 'u1', + model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, + admitted_at: new Date(now.getTime() - 19 * 60 * 60 * 1000), + }) + + const state = await getSessionState({ userId: 'u1', deps }) + expect(state.status).toBe('none') + if (state.status !== 'none') throw new Error('unreachable') + expect( + state.rateLimitsByModel?.[FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID], + ).toEqual({ + model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, + limit: FREEBUFF_PREMIUM_SESSION_LIMIT, + windowHours: FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS, + recentCount: 1, + }) + }) + test('active session with matching instance id returns active', async () => { await requestSession({ userId: 'u1', model: DEFAULT_MODEL, deps }) const row = deps.rows.get('u1')! @@ -740,7 +875,11 @@ describe('getSessionState', () => { model: 'deepseek/deepseek-v4-pro', admitted_at: new Date(now.getTime() - 60 * 60 * 1000), }) - await requestSession({ userId: 'u1', model: 'deepseek/deepseek-v4-pro', deps }) + await requestSession({ + userId: 'u1', + model: 'deepseek/deepseek-v4-pro', + deps, + }) const row = deps.rows.get('u1')! row.status = 'active' row.admitted_at = now @@ -753,23 +892,27 @@ describe('getSessionState', () => { }) if (state.status !== 'active') throw new Error('unreachable') expect(state.rateLimit).toEqual({ - model: 'deepseek/deepseek-v4-pro', - limit: 5, - windowHours: 18, + model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, + limit: FREEBUFF_PREMIUM_SESSION_LIMIT, + windowHours: FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS, recentCount: 1, }) }) - test('active session only fetches quota for its own model', async () => { + test('active session only fetches one shared premium quota snapshot', async () => { deps._tick(new Date('2026-04-17T16:00:00Z')) let listRecentAdmitsCalls = 0 - const originalListRecentAdmits = deps.listRecentAdmits - deps.listRecentAdmits = async (params) => { + const originalListRecentAdmits = deps.listRecentPremiumAdmits + deps.listRecentPremiumAdmits = async (params) => { listRecentAdmitsCalls++ return originalListRecentAdmits(params) } - await requestSession({ userId: 'u1', model: 'deepseek/deepseek-v4-pro', deps }) + await requestSession({ + userId: 'u1', + model: 'deepseek/deepseek-v4-pro', + deps, + }) const row = deps.rows.get('u1')! row.status = 'active' row.admitted_at = deps._now() @@ -1117,6 +1260,23 @@ describe('endUserSession', () => { expect(deps.rows.has('u1')).toBe(false) }) + test('rounds active premium session usage up to nearest tenth on early end', async () => { + const deps = makeDeps({ getInstantAdmitCapacity: () => 3 }) + deps._tick(new Date('2026-04-17T16:00:00Z')) + const state = await requestSession({ + userId: 'u1', + model: FREEBUFF_KIMI_MODEL_ID, + deps, + }) + expect(state.status).toBe('active') + deps._tick(new Date(deps._now().getTime() + 14 * 60 * 1000)) + + await endUserSession({ userId: 'u1', deps }) + + expect(deps.rows.has('u1')).toBe(false) + 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', { diff --git a/web/src/server/free-session/public-api.ts b/web/src/server/free-session/public-api.ts index 52d5d442b4..a1a065abec 100644 --- a/web/src/server/free-session/public-api.ts +++ b/web/src/server/free-session/public-api.ts @@ -3,9 +3,11 @@ import { FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, FREEBUFF_DEPLOYMENT_HOURS_LABEL, FREEBUFF_GEMINI_PRO_MODEL_ID, - FREEBUFF_GLM_MODEL_ID, - FREEBUFF_KIMI_MODEL_ID, + FREEBUFF_PREMIUM_MODEL_IDS, + FREEBUFF_PREMIUM_SESSION_LIMIT, + FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS, isFreebuffModelAvailable, + isFreebuffPremiumModelId, isSupportedFreebuffModelId, resolveSupportedFreebuffModel, } from '@codebuff/common/constants/freebuff-models' @@ -23,7 +25,7 @@ import { FreeSessionModelLockedError, getSessionRow, joinOrTakeOver, - listRecentAdmits, + listRecentPremiumAdmits, promoteQueuedUser, queueDepthsByModel, queuePositionFor, @@ -40,72 +42,106 @@ import type { SessionStateResponse, } from './types' -/** - * Per-model admission rate limits. Keyed by freebuff model id; a model not - * in the map has no rate limit applied. Minimax is cheap enough to leave - * unlimited. - * - * Hard-coded rather than env-driven: the values need to be observable in the - * code review, and the CLI already renders the numbers via `rateLimit` on - * queued/active responses — changing them is a deliberate, typed edit. - */ -const RATE_LIMITS: Record = { - [FREEBUFF_GLM_MODEL_ID]: { limit: 5, windowHours: 12 }, - [FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: { limit: 5, windowHours: 18 }, - [FREEBUFF_KIMI_MODEL_ID]: { limit: 5, windowHours: 18 }, +function roundSessionUnits(units: number): number { + return Math.round(units * 10) / 10 } -/** Fetch the caller's current quota snapshot for `model`, or undefined if the - * model isn't rate-limited. Used by both POST (after admit) and GET polls so - * the CLI's "N of M sessions used" line stays live instead of disappearing - * after the first poll. Also returns the oldest admit in-window and the - * window duration so callers that need `retryAfterMs` don't have to re-query - * or duplicate the window math. */ -async function fetchRateLimitSnapshot( +function getRetryAfterMsForPremiumLimit(params: { + admits: Awaited> + totalUnits: number + targetUnits: number + windowMs: number + now: Date +}): number { + let remainingUnits = params.totalUnits + for (const admit of params.admits) { + remainingUnits = roundSessionUnits(remainingUnits - admit.sessionUnits) + if (remainingUnits <= params.targetUnits) { + return Math.max( + 0, + admit.admittedAt.getTime() + params.windowMs - params.now.getTime(), + ) + } + } + return 0 +} + +function canStartPremiumSession(snapshot: FreebuffSessionRateLimit): boolean { + return snapshot.recentCount < snapshot.limit +} + +interface PremiumQuotaSnapshot { + recentCount: number + admits: Awaited> + windowMs: number +} + +async function fetchPremiumQuotaSnapshot( userId: string, - model: string, deps: SessionDeps, -): Promise< - | { info: FreebuffSessionRateLimit; oldest: Date | null; windowMs: number } - | undefined -> { - const cfg = RATE_LIMITS[model] - if (!cfg) return undefined +): Promise { const now = nowOf(deps) - const windowMs = cfg.windowHours * 60 * 60 * 1000 + const windowMs = FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS * 60 * 60 * 1000 const since = new Date(now.getTime() - windowMs) - const admits = await deps.listRecentAdmits({ + const admits = await deps.listRecentPremiumAdmits({ userId, - model, since, - limit: cfg.limit, + models: FREEBUFF_PREMIUM_MODEL_IDS, }) return { - info: { - model, - limit: cfg.limit, - windowHours: cfg.windowHours, - recentCount: admits.length, - }, - oldest: admits[0] ?? null, + recentCount: roundSessionUnits( + admits.reduce((sum, admit) => sum + admit.sessionUnits, 0), + ), + admits, windowMs, } } +function toRateLimitInfo( + model: string, + snapshot: PremiumQuotaSnapshot, +): FreebuffSessionRateLimit { + return { + model, + limit: FREEBUFF_PREMIUM_SESSION_LIMIT, + windowHours: FREEBUFF_PREMIUM_SESSION_WINDOW_HOURS, + recentCount: snapshot.recentCount, + } +} + +/** Fetch the caller's current shared premium-session quota snapshot for + * `model`, or undefined if the model is unlimited. Used by both POST (after + * admit) and GET polls so the CLI's "N of M sessions used" line stays live + * instead of disappearing after the first poll. */ +async function fetchRateLimitSnapshot( + userId: string, + model: string, + deps: SessionDeps, +): Promise< + | { + info: FreebuffSessionRateLimit + admits: Awaited> + windowMs: number + } + | undefined +> { + if (!isFreebuffPremiumModelId(model)) return undefined + const snapshot = await fetchPremiumQuotaSnapshot(userId, deps) + return { + info: toRateLimitInfo(model, snapshot), + admits: snapshot.admits, + windowMs: snapshot.windowMs, + } +} + async function fetchRateLimitsByModel( userId: string, deps: SessionDeps, ): Promise> { - const entries = await Promise.all( - Object.keys(RATE_LIMITS).map(async (model) => { - const snapshot = await fetchRateLimitSnapshot(userId, model, deps) - return snapshot ? ([model, snapshot.info] as const) : null - }), - ) + const snapshot = await fetchPremiumQuotaSnapshot(userId, deps) return Object.fromEntries( - entries.filter( - (entry): entry is readonly [string, FreebuffSessionRateLimit] => - entry !== null, + FREEBUFF_PREMIUM_MODEL_IDS.map( + (model) => [model, toRateLimitInfo(model, snapshot)] as const, ), ) } @@ -134,7 +170,11 @@ export interface SessionDeps { now: Date countryAccess?: FreeSessionCountryAccessMetadata }) => Promise - endSession: (userId: string) => Promise + endSession: (params: { + userId: string + now: Date + sessionLengthMs: number + }) => Promise queueDepthsByModel: () => Promise> queuePositionFor: (params: { userId: string @@ -145,15 +185,12 @@ export interface SessionDeps { * bound to a given model. Compared against the model's configured * `instantAdmitCapacity` to decide whether a new joiner skips the queue. */ activeCountForModel: (model: string) => Promise - /** Rate-limit helper: oldest-first admission timestamps for (userId, model) - * inside the window. The caller uses `rows.length` as the count (capped - * at `limit`) and `rows[0]` as the oldest for `retryAfterMs`. */ - listRecentAdmits: (params: { + /** Rate-limit helper: oldest-first premium admissions inside the window. */ + listRecentPremiumAdmits: (params: { userId: string - model: string + models: readonly string[] since: Date - limit: number - }) => Promise + }) => Promise<{ admittedAt: Date; model: string; sessionUnits: number }[]> /** Instant-admit promotion: flips a specific queued row to active. Returns * the updated row or null if the row wasn't in a queued state. */ promoteQueuedUser: (params: { @@ -182,7 +219,7 @@ const defaultDeps: SessionDeps = { queueDepthsByModel, queuePositionFor, activeCountForModel, - listRecentAdmits, + listRecentPremiumAdmits, promoteQueuedUser, getInstantAdmitCapacity, isWaitingRoomEnabled, @@ -291,8 +328,8 @@ export async function requestSession(params: { } // Rate-limit check runs before joinOrTakeOver so heavy users never even - // create a queued row. Only models listed in RATE_LIMITS are gated; others - // (Minimax today) fall through unchanged. + // create a queued row. Premium models share one 20h session-unit pool; + // Minimax falls through unchanged as unlimited. // // Takeover/reclaim exception: a user who already holds a queued or // active+unexpired row on this same model is re-anchoring (CLI restart, @@ -319,13 +356,14 @@ export async function requestSession(params: { if (!isReclaim) { const snapshot = await fetchRateLimitSnapshot(params.userId, model, deps) - if (snapshot && snapshot.info.recentCount >= snapshot.info.limit) { - // Oldest admit's window-anniversary is when one slot opens back up. - // Clamped at 0 so a clock skew can't surface a negative retry-after. - const retryAfterMs = Math.max( - 0, - (snapshot.oldest?.getTime() ?? 0) + snapshot.windowMs - now.getTime(), - ) + if (snapshot && !canStartPremiumSession(snapshot.info)) { + const retryAfterMs = getRetryAfterMsForPremiumLimit({ + admits: snapshot.admits, + totalUnits: snapshot.info.recentCount, + targetUnits: snapshot.info.limit, + windowMs: snapshot.windowMs, + now, + }) return { status: 'rate_limited', model, @@ -493,7 +531,11 @@ export async function endUserSession(params: { ) { return } - await deps.endSession(params.userId) + await deps.endSession({ + userId: params.userId, + now: nowOf(deps), + sessionLengthMs: deps.sessionLengthMs, + }) } export type SessionGateResult = diff --git a/web/src/server/free-session/store.ts b/web/src/server/free-session/store.ts index 1a8d2dba0c..660f7a34a7 100644 --- a/web/src/server/free-session/store.ts +++ b/web/src/server/free-session/store.ts @@ -1,7 +1,7 @@ import { db } from '@codebuff/internal/db' import { coerceBool } from '@codebuff/internal/db/advisory-lock' import * as schema from '@codebuff/internal/db/schema' -import { and, asc, count, eq, gte, lt, sql } from 'drizzle-orm' +import { and, asc, count, desc, eq, gte, inArray, lt, sql } from 'drizzle-orm' import { FREEBUFF_ADMISSION_LOCK_ID } from './config' @@ -161,10 +161,70 @@ export async function joinOrTakeOver(params: { return row as InternalSessionRow } -export async function endSession(userId: string): Promise { - await db - .delete(schema.freeSession) - .where(eq(schema.freeSession.user_id, userId)) +export function getRoundedSessionUnits(params: { + admittedAt: Date | null + now: Date + sessionLengthMs: number +}): number { + const { admittedAt, now, sessionLengthMs } = params + if (!admittedAt || sessionLengthMs <= 0) return 0 + const usedMs = Math.max( + 0, + Math.min(sessionLengthMs, now.getTime() - admittedAt.getTime()), + ) + return Math.ceil((usedMs / sessionLengthMs) * 10) / 10 +} + +export async function endSession(params: { + userId: string + now: Date + sessionLengthMs: number +}): Promise { + const { userId, now, sessionLengthMs } = params + await db.transaction(async (tx) => { + const [row] = await tx + .select() + .from(schema.freeSession) + .where(eq(schema.freeSession.user_id, userId)) + .for('update') + .limit(1) + + if ( + row?.status === 'active' && + row.admitted_at && + row.expires_at && + row.expires_at.getTime() > now.getTime() + ) { + const sessionUnits = getRoundedSessionUnits({ + admittedAt: row.admitted_at, + now, + sessionLengthMs, + }).toFixed(1) + + const [latestAdmit] = await tx + .select({ id: schema.freeSessionAdmit.id }) + .from(schema.freeSessionAdmit) + .where( + and( + eq(schema.freeSessionAdmit.user_id, userId), + eq(schema.freeSessionAdmit.model, row.model), + ), + ) + .orderBy(desc(schema.freeSessionAdmit.admitted_at)) + .limit(1) + + if (latestAdmit) { + await tx + .update(schema.freeSessionAdmit) + .set({ session_units: sessionUnits }) + .where(eq(schema.freeSessionAdmit.id, latestAdmit.id)) + } + } + + await tx + .delete(schema.freeSession) + .where(eq(schema.freeSession.user_id, userId)) + }) } export async function queueDepth(params: { model: string }): Promise { @@ -459,36 +519,44 @@ export async function promoteQueuedUser(params: { }) } +export interface RecentSessionAdmit { + admittedAt: Date + model: string + sessionUnits: number +} + /** - * List admissions for `userId` on `model` whose `admitted_at` is within the - * window `[since, ∞)`, ordered oldest-first. Caller gets both the count - * (array length, capped at `limit`) and the oldest timestamp (`rows[0]`) — - * the oldest is needed to compute `retryAfterMs` when the window is full, - * so one query covers both the check and the reject path. - * - * Drives the per-user, per-model rate limit (e.g. at most 5 DeepSeek sessions - * in the last 12h) enforced before `joinOrTakeOver`. + * List premium-model admissions for `userId` inside `[since, ∞)`, ordered + * oldest-first. Each row carries charged session units; manual early end can + * revise a freshly written 1.0-unit admit down to a fractional value. */ -export async function listRecentAdmits(params: { +export async function listRecentPremiumAdmits(params: { userId: string - model: string + models: readonly string[] since: Date - limit: number -}): Promise { - const { userId, model, since, limit } = params +}): Promise { + const { userId, models, since } = params + if (models.length === 0) return [] const rows = await db - .select({ admitted_at: schema.freeSessionAdmit.admitted_at }) + .select({ + admitted_at: schema.freeSessionAdmit.admitted_at, + model: schema.freeSessionAdmit.model, + session_units: schema.freeSessionAdmit.session_units, + }) .from(schema.freeSessionAdmit) .where( and( eq(schema.freeSessionAdmit.user_id, userId), - eq(schema.freeSessionAdmit.model, model), + inArray(schema.freeSessionAdmit.model, [...models]), gte(schema.freeSessionAdmit.admitted_at, since), ), ) .orderBy(asc(schema.freeSessionAdmit.admitted_at)) - .limit(limit) - return rows.map((r) => r.admitted_at) + return rows.map((r) => ({ + admittedAt: r.admitted_at, + model: r.model, + sessionUnits: Number(r.session_units), + })) } /** Stable 31-bit hash so model-keyed advisory lock ids don't overflow int4. */ From f78771ebb08897119a4f6e93d03b1b9c891e9f51 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 4 May 2026 16:56:52 -0700 Subject: [PATCH 10/14] Remove redundant "free session" text --- cli/src/components/status-bar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/src/components/status-bar.tsx b/cli/src/components/status-bar.tsx index 82c2b16d8f..945f768be0 100644 --- a/cli/src/components/status-bar.tsx +++ b/cli/src/components/status-bar.tsx @@ -188,8 +188,7 @@ export const StatusBar = ({ return ( {modelName ? `${modelName} · ` : ''} - {quotaText}Free session ·{' '} - {formatSessionRemaining(sessionProgress.remainingMs)} + {quotaText}{formatSessionRemaining(sessionProgress.remainingMs)} ) } From 6877b739db72f5b34caf962a08c4c88e7a9939c3 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 4 May 2026 17:00:28 -0700 Subject: [PATCH 11/14] Fix types --- cli/src/utils/__tests__/sdk-event-handlers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/utils/__tests__/sdk-event-handlers.test.ts b/cli/src/utils/__tests__/sdk-event-handlers.test.ts index 8f34427b1d..051a596893 100644 --- a/cli/src/utils/__tests__/sdk-event-handlers.test.ts +++ b/cli/src/utils/__tests__/sdk-event-handlers.test.ts @@ -39,7 +39,7 @@ interface ToolResultEvent { type: 'json' value: Array<{ agentName: string - value: string + value: any }> }> } From 011dee455fd43a29e5999ab71504a5ab3c1d85cc Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 4 May 2026 17:26:11 -0700 Subject: [PATCH 12/14] Better show sessions used --- .../components/freebuff-model-selector.tsx | 49 ++++++++++++------- cli/src/components/status-bar.tsx | 11 +---- cli/src/components/waiting-room-screen.tsx | 12 ----- common/src/constants/freebuff-models.ts | 2 +- 4 files changed, 34 insertions(+), 40 deletions(-) diff --git a/cli/src/components/freebuff-model-selector.tsx b/cli/src/components/freebuff-model-selector.tsx index c3111b2770..24f87350e8 100644 --- a/cli/src/components/freebuff-model-selector.tsx +++ b/cli/src/components/freebuff-model-selector.tsx @@ -98,19 +98,26 @@ export const FreebuffModelSelector: React.FC = () => { ? session.rateLimitsByModel : undefined - const getQuotaHint = useCallback( - (modelId: string): string => { - const rateLimit = rateLimitsByModel?.[modelId] - if (rateLimit) { - return `${formatSessionUnits(rateLimit.recentCount)}/${rateLimit.limit} used` - } - return isFreebuffPremiumModelId(modelId) - ? `0/${FREEBUFF_PREMIUM_SESSION_LIMIT} used` - : 'Unlimited' - }, + // All premium models share one quota pool: the server replicates the same + // snapshot under each premium model id, so any entry has the right count. + // Grab the first one (or 0 when the user has no usage and the map is + // absent) so the footer can render the single shared counter. + const sharedPremiumUsed = useMemo( + () => + rateLimitsByModel + ? (Object.values(rateLimitsByModel)[0]?.recentCount ?? 0) + : 0, [rateLimitsByModel], ) + // Per-row hint is a tier badge, not a quota counter: premium models share + // the 5-session pool (shown once in the footer); MiniMax is unlimited. + const getTierLabel = useCallback( + (modelId: string): string => + isFreebuffPremiumModelId(modelId) ? 'Premium' : 'Unlimited', + [], + ) + const BUTTON_CHROME = 4 // 2 border + 2 padding // Decide whether secondary details (warning / deployment hours) get their @@ -130,7 +137,7 @@ export const FreebuffModelSelector: React.FC = () => { } const hintLen = (model: FreebuffModelOption): number => - Math.max(getQuotaHint(model.id).length, 'Closed'.length) + Math.max(getTierLabel(model.id).length, 'Closed'.length) const oneLineLen = (model: FreebuffModelOption): number => { const inlineDetails = detailsTextLen(model) @@ -140,7 +147,7 @@ export const FreebuffModelSelector: React.FC = () => { 3 /* " · " */ + model.tagline.length + (inlineDetails > 0 ? 3 + inlineDetails : 0) + - 1 /* space before hint */ + + 3 /* " · " before hint */ + hintLen(model) ) } @@ -150,7 +157,7 @@ export const FreebuffModelSelector: React.FC = () => { model.displayName.length + 3 + model.tagline.length + - 1 + + 3 + hintLen(model) const detailsLineLen = (model: FreebuffModelOption): number => { @@ -176,7 +183,7 @@ export const FreebuffModelSelector: React.FC = () => { contentMaxWidth, ), } - }, [contentMaxWidth, deploymentAvailabilityLabel, getQuotaHint]) + }, [contentMaxWidth, deploymentAvailabilityLabel, getTierLabel]) const isJoinable = useCallback( (modelId: string) => { @@ -255,8 +262,8 @@ export const FreebuffModelSelector: React.FC = () => { // anything except re-picking the queue we're already in. const interactable = !pending && canJoin && model.id !== committedModelId - const quotaHint = getQuotaHint(model.id) - const hint = isAvailable ? quotaHint : 'Closed' + const tierLabel = getTierLabel(model.id) + const hint = isAvailable ? tierLabel : 'Closed' // Focused row: green border + arrow indicator + bold name. The name // itself stays the normal foreground color so it doesn't shout — the @@ -317,7 +324,7 @@ export const FreebuffModelSelector: React.FC = () => { {showInlineWarning && ( · {model.warning} )} - {hint} + · {hint} {showWrappedDetails && ( @@ -336,6 +343,14 @@ export const FreebuffModelSelector: React.FC = () => { ) })} + {/* Single shared-quota footer. Replaces the per-row "X/5 used" hints + which made it look like each premium model had its own pool. + wrapMode: 'word' so the line reflows on narrow terminals instead of + clipping. */} + + {formatSessionUnits(sharedPremiumUsed)} /{' '} + {FREEBUFF_PREMIUM_SESSION_LIMIT} premium sessions used today + ) } diff --git a/cli/src/components/status-bar.tsx b/cli/src/components/status-bar.tsx index 945f768be0..11e7f7875e 100644 --- a/cli/src/components/status-bar.tsx +++ b/cli/src/components/status-bar.tsx @@ -66,9 +66,6 @@ const formatSessionRemaining = (ms: number): string => { return minutes === 0 ? `${hours}h left` : `${hours}h ${minutes}m left` } -const formatSessionUnits = (units: number): string => - Number.isInteger(units) ? String(units) : units.toFixed(1) - interface StatusBarProps { timerStartTime: number | null isAtBottom: boolean @@ -179,16 +176,10 @@ export const StatusBar = ({ freebuffSession?.status === 'active' ? getFreebuffModel(freebuffSession.model).displayName : null - const quotaText = - freebuffSession?.status === 'active' && freebuffSession.rateLimit - ? `Premium ${formatSessionUnits(freebuffSession.rateLimit.recentCount)}/${freebuffSession.rateLimit.limit} used · ` - : freebuffSession?.status === 'active' - ? 'Unlimited · ' - : '' return ( {modelName ? `${modelName} · ` : ''} - {quotaText}{formatSessionRemaining(sessionProgress.remainingMs)} + {formatSessionRemaining(sessionProgress.remainingMs)} ) } diff --git a/cli/src/components/waiting-room-screen.tsx b/cli/src/components/waiting-room-screen.tsx index 36de9a86d0..839e780c68 100644 --- a/cli/src/components/waiting-room-screen.tsx +++ b/cli/src/components/waiting-room-screen.tsx @@ -263,18 +263,6 @@ export const WaitingRoomScreen: React.FC = ({ Elapsed {formatElapsed(elapsedMs)} - {/* Premium session quota. Minimax is unlimited, so it has no - rateLimit payload and skips this line. */} - {session.rateLimit && ( - - Premium sessions - - {formatSessionUnits(session.rateLimit.recentCount)} /{' '} - {session.rateLimit.limit} - - used in the last 20 hours - - )} )} diff --git a/common/src/constants/freebuff-models.ts b/common/src/constants/freebuff-models.ts index 3f96183287..fedd5154cf 100644 --- a/common/src/constants/freebuff-models.ts +++ b/common/src/constants/freebuff-models.ts @@ -80,7 +80,7 @@ export const FREEBUFF_MODELS = [ { id: FREEBUFF_MINIMAX_MODEL_ID, displayName: 'MiniMax M2.7', - tagline: 'Fastest, unlimited', + tagline: 'Fastest', availability: 'always', }, ] as const satisfies readonly FreebuffModelOption[] From 3d840152336703af7a85ef87c2537078f10ad855 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 4 May 2026 17:35:56 -0700 Subject: [PATCH 13/14] Exclude tool call errors from last_message and all_messages subagent output --- .../agent-runtime/src/tools/stream-parser.ts | 14 ++++---- .../agent-runtime/src/util/agent-output.ts | 32 +++++++++++++++---- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/agent-runtime/src/tools/stream-parser.ts b/packages/agent-runtime/src/tools/stream-parser.ts index fa4c4e4210..4cdb32117e 100644 --- a/packages/agent-runtime/src/tools/stream-parser.ts +++ b/packages/agent-runtime/src/tools/stream-parser.ts @@ -114,11 +114,12 @@ export async function processStream( if (chunk.type === 'error') { hadToolCallError = true errorMessages.push( - userMessage( - withSystemTags( + userMessage({ + content: withSystemTags( `Error during tool call: ${chunk.message}. Please check the tool name and arguments and try again.`, ), - ), + tags: ['TOOL_CALL_ERROR'], + }), ) } } @@ -304,11 +305,12 @@ export async function processStream( onResponseChunk(chunk) hadToolCallError = true errorMessages.push( - userMessage( - withSystemTags( + userMessage({ + content: withSystemTags( `Error during tool call: ${chunk.message}. Please check the tool name and arguments and try again.`, ), - ), + tags: ['TOOL_CALL_ERROR'], + }), ) } else if (chunk.type === 'tool-call') { } else { diff --git a/packages/agent-runtime/src/util/agent-output.ts b/packages/agent-runtime/src/util/agent-output.ts index fe3a8da0a6..95919daa68 100644 --- a/packages/agent-runtime/src/util/agent-output.ts +++ b/packages/agent-runtime/src/util/agent-output.ts @@ -5,12 +5,29 @@ import type { AgentOutput, } from '@codebuff/common/types/session-state' +/** Messages tagged with these tags are stripped from agent output. */ +const EXCLUDED_OUTPUT_TAGS = ['TOOL_CALL_ERROR'] as const + +function isExcludedFromOutput(message: Message): boolean { + return !!message.tags?.some((t) => + (EXCLUDED_OUTPUT_TAGS as readonly string[]).includes(t), + ) +} + /** - * Get the last assistant turn messages, which includes the last assistant message - * and any subsequent tool messages that are responses to its tool calls. + * Get the last assistant turn messages, which includes the last assistant + * message and any subsequent tool messages that are responses to its tool + * calls. + * + * Turn selection walks the raw `messageHistory` so that user-role messages + * (including synthesized TOOL_CALL_ERROR ones) correctly bound the turn — + * otherwise a failed attempt + its retry would get conflated into a single + * "turn". Exclusion filtering is applied *after* selection: TOOL_CALL_ERROR + * messages are user-role so they never enter `result` anyway (the role check + * below stops at user messages), but keeping the filter explicit documents + * the contract that no excluded tags leak into agent output. */ function getLastAssistantTurnMessages(messageHistory: Message[]): Message[] { - // Find the index of the last assistant message let lastAssistantIndex = -1 for (let i = messageHistory.length - 1; i >= 0; i--) { if (messageHistory[i].role === 'assistant') { @@ -29,19 +46,18 @@ function getLastAssistantTurnMessages(messageHistory: Message[]): Message[] { return [] } - // Collect the assistant message and all subsequent tool messages const result: Message[] = [] for (let i = lastAssistantIndex; i < messageHistory.length; i++) { const message = messageHistory[i] if (message.role === 'assistant' || message.role === 'tool') { result.push(message) } else { - // Stop if we hit a user or system message + // Stop if we hit a user or system message. break } } - return result + return result.filter((m) => !isExcludedFromOutput(m)) } export function getAgentOutput( @@ -71,7 +87,9 @@ export function getAgentOutput( } if (agentTemplate.outputMode === 'all_messages') { // Remove the first message, which includes the previous conversation history. - const agentMessages = agentState.messageHistory.slice(1) + const agentMessages = agentState.messageHistory + .slice(1) + .filter((m) => !isExcludedFromOutput(m)) return { type: 'allMessages', value: agentMessages, From a7101da49e30f0f1d6f3e287bad809e6e57b1cf6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 5 May 2026 00:40:12 +0000 Subject: [PATCH 14/14] Bump Freebuff version to 0.0.77 --- freebuff/cli/release/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freebuff/cli/release/package.json b/freebuff/cli/release/package.json index 0d9a450127..eef9985665 100644 --- a/freebuff/cli/release/package.json +++ b/freebuff/cli/release/package.json @@ -1,6 +1,6 @@ { "name": "freebuff", - "version": "0.0.76", + "version": "0.0.77", "description": "The world's strongest free coding agent", "license": "MIT", "bin": {