Expose per-account session counts and cooldowns in account listing

The status endpoint now reports, per account, how many sessions are
bound with traffic inside the routing load window, plus the account's
cooldown deadline. The coderouter CLI uses both to make sticky routing
and rate-limit cooldowns visible. The session-count read is display
only and fails to zero so it can never take the status endpoint down.
This commit is contained in:
Lawrence Chen
2026-08-19 03:59:50 -07:00
parent 882ab10e75
commit 34f0e1eeca
3 changed files with 75 additions and 16 deletions
+50 -16
View File
@@ -151,22 +151,56 @@ export async function deleteAccount(input: {
export async function listAccounts(
teamId: string,
): Promise<readonly CodeRouterAccountSummary[]> {
return await cloudDb()
.select({
id: coderouterAccounts.id,
provider: coderouterAccounts.provider,
providerAccountId: coderouterAccounts.providerAccountId,
label: coderouterAccounts.label,
state: coderouterAccounts.state,
credentialExpiresAt: coderouterAccounts.credentialExpiresAt,
lastFailureCode: coderouterAccounts.lastFailureCode,
})
.from(coderouterAccounts)
.where(eq(coderouterAccounts.teamId, teamId))
.then((rows) => rows.map((row) => ({
...row,
credentialExpiresAt: row.credentialExpiresAt?.toISOString() ?? null,
})));
const [rows, sessionCounts] = await Promise.all([
cloudDb()
.select({
id: coderouterAccounts.id,
provider: coderouterAccounts.provider,
providerAccountId: coderouterAccounts.providerAccountId,
label: coderouterAccounts.label,
state: coderouterAccounts.state,
credentialExpiresAt: coderouterAccounts.credentialExpiresAt,
lastFailureCode: coderouterAccounts.lastFailureCode,
cooldownUntil: coderouterAccounts.cooldownUntil,
})
.from(coderouterAccounts)
.where(eq(coderouterAccounts.teamId, teamId)),
countActiveSessionsByAccount(teamId),
]);
return rows.map((row) => ({
...row,
credentialExpiresAt: row.credentialExpiresAt?.toISOString() ?? null,
cooldownUntil: row.cooldownUntil?.toISOString() ?? null,
activeSessions: sessionCounts.get(row.id) ?? 0,
}));
}
/**
* Sessions bound per account with traffic inside the load window. Display
* only: a failure here must never take the status endpoint down.
*/
async function countActiveSessionsByAccount(
teamId: string,
): Promise<ReadonlyMap<string, number>> {
try {
const rows = await cloudDb()
.select({
accountId: coderouterSessionAccounts.accountId,
sessions: sql<number>`count(*)::int`,
})
.from(coderouterSessionAccounts)
.where(and(
eq(coderouterSessionAccounts.teamId, teamId),
gt(
coderouterSessionAccounts.lastSeenAt,
sql`now() - interval '${sql.raw(SESSION_BINDING_LOAD_WINDOW)}'`,
),
))
.groupBy(coderouterSessionAccounts.accountId);
return new Map(rows.map((row) => [row.accountId, Number(row.sessions)]));
} catch {
return new Map();
}
}
export async function listCoderouterTeamIds(): Promise<readonly string[]> {
+3
View File
@@ -41,4 +41,7 @@ export type CodeRouterAccountSummary = {
readonly state: "active" | "refreshing" | "expired" | "broken";
readonly credentialExpiresAt: string | null;
readonly lastFailureCode: string | null;
readonly cooldownUntil: string | null;
/** Sessions bound to this account with traffic in the recent window. */
readonly activeSessions: number;
};
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
import { randomUUID } from "node:crypto";
import postgres, { type Sql } from "postgres";
import { closeCloudDbForTests } from "../db/client";
import { listAccounts } from "../services/coderouter/repository";
import {
bindSessionAccount,
claimAccountForPlacement,
@@ -213,6 +214,27 @@ describe("coderouter routing db behavior", () => {
expect(during?.sticky).toBe(true);
});
dbTest("account listing reports active session counts and cooldowns", async () => {
if (!sql) throw new Error("no sql client");
const accounts = await insertAccounts(2);
await bindSessionAccount(TEAM, "codex", "fresh-session-1", accounts[0] ?? "");
await bindSessionAccount(TEAM, "codex", "fresh-session-2", accounts[0] ?? "");
await bindSessionAccount(TEAM, "codex", "stale-session", accounts[1] ?? "");
await sql`
update coderouter_session_accounts
set last_seen_at = now() - interval '7 hours'
where session_key = 'stale-session'
`;
await markAccountCooldown(accounts[1] ?? "", 60_000);
const listed = await listAccounts(TEAM);
const byId = new Map(listed.map((account) => [account.id, account]));
expect(byId.get(accounts[0] ?? "")?.activeSessions).toBe(2);
expect(byId.get(accounts[0] ?? "")?.cooldownUntil).toBeNull();
// A binding idle beyond the load window no longer counts.
expect(byId.get(accounts[1] ?? "")?.activeSessions).toBe(0);
expect(byId.get(accounts[1] ?? "")?.cooldownUntil).not.toBeNull();
});
dbTest("routes without stickiness while the session table is missing", async () => {
if (!sql) throw new Error("no sql client");
await insertAccounts(2);