From da75923f8cd3eed4df2c3b1a855b00a71b291cc3 Mon Sep 17 00:00:00 2001 From: Maxwell Weinzierl Date: Sun, 15 Nov 2026 06:25:56 -0800 Subject: [PATCH 1/2] t4: with every Claude account used up and no main login, give the one that frees first (AI-99) A token-less main made Claude answer 'Not logged in', a failed turn nothing resumes; usa-rs threads sat 17 h so on 2026-11-14. Now Claude answers with its real limit, which limit recovery marks and resumes at the reset. --- apps/server/src/t4/claudeAccounts.test.ts | 15 +++++++++++++++ apps/server/src/t4/claudeAccounts.ts | 20 +++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/apps/server/src/t4/claudeAccounts.test.ts b/apps/server/src/t4/claudeAccounts.test.ts index 38227bfa372af..2824d4a1aa2c0 100644 --- a/apps/server/src/t4/claudeAccounts.test.ts +++ b/apps/server/src/t4/claudeAccounts.test.ts @@ -135,6 +135,21 @@ describe("t4 claudeAccounts", () => { expect(pickClaudeAccount(1_000, noLogin)).toEqual({ key: "main" }); }); + it("with every account used up and no main login, gives the one that frees first (AI-99)", () => { + const { dir, env } = setup(); + const home = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t4-home-")); + const noLogin = { ...env, HOME: home } as NodeJS.ProcessEnv; + markClaudeAccountExhausted("1", 90_000, 1_000, noLogin); + markClaudeAccountExhausted("2", 50_000, 1_000, noLogin); + expect(pickClaudeAccount(1_000, noLogin)).toEqual({ key: "2", token: "token-two" }); + // A token file gone is skipped there too. + NodeFS.rmSync(NodePath.join(dir, "2.token")); + expect(pickClaudeAccount(1_000, noLogin)).toEqual({ key: "1", token: "token-one" }); + // No usable account at all: the main login, as before. + NodeFS.rmSync(NodePath.join(dir, "1.token")); + expect(pickClaudeAccount(1_000, noLogin)).toEqual({ key: "main" }); + }); + it("signs the status check and text generation in with an account, used up or not", () => { const { dir, env } = setup(); const home = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t4-home-")); diff --git a/apps/server/src/t4/claudeAccounts.ts b/apps/server/src/t4/claudeAccounts.ts index 27ec78a79ae97..c854eb608b23d 100644 --- a/apps/server/src/t4/claudeAccounts.ts +++ b/apps/server/src/t4/claudeAccounts.ts @@ -193,7 +193,13 @@ export const isClaudeAccountExhausted = ( return blockedUntil(loadState(env, files), key, model) > nowMs; }; -/** The first account in order not used up for `model` (the main login included), else the main login. */ +/** + * The first account in order not used up for `model` (the main login included). With every one + * used up: the main login when it exists, else the account whose mark ends first. Claude then + * answers with its real limit, which T3's limit recovery marks and resumes at the reset, or runs + * if the mark was stale. A token-less "main" made Claude answer "Not logged in", a failed turn + * nothing resumes: usa-rs threads sat 17 h so on 2026-11-14 (AI-99). + */ export const pickClaudeAccount = ( nowMs: number, env: NodeJS.ProcessEnv = process.env, @@ -202,16 +208,20 @@ export const pickClaudeAccount = ( const files = accountFiles(env); if (files.length === 0) return { key: MAIN_CLAUDE_ACCOUNT }; const state = loadState(env, files); + let soonest: { account: ClaudeAccount; until: number } | null = null; for (const { key, file } of accountList(env)) { - if (blockedUntil(state, key, model) > nowMs) continue; + const until = blockedUntil(state, key, model); if (file === undefined) { - if (mainLoginAvailable(env)) return { key: MAIN_CLAUDE_ACCOUNT }; + if (until <= nowMs && mainLoginAvailable(env)) return { key: MAIN_CLAUDE_ACCOUNT }; continue; } const token = readToken(file); - if (token !== undefined) return { key, token }; + if (token === undefined) continue; + if (until <= nowMs) return { key, token }; + if (soonest === null || until < soonest.until) soonest = { account: { key, token }, until }; } - return { key: MAIN_CLAUDE_ACCOUNT }; + if (mainLoginAvailable(env) || soonest === null) return { key: MAIN_CLAUDE_ACCOUNT }; + return soonest.account; }; export const claudeAccountEnvironment = ( From ae9b619160932ce058069f786705d3cee8a05a56 Mon Sep 17 00:00:00 2001 From: Maxwell Weinzierl Date: Sun, 15 Nov 2026 06:30:04 -0800 Subject: [PATCH 2/2] t4: shorten the pickClaudeAccount comment for lint (AI-99) --- apps/server/src/t4/claudeAccounts.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/server/src/t4/claudeAccounts.ts b/apps/server/src/t4/claudeAccounts.ts index c854eb608b23d..9c39ed75021b5 100644 --- a/apps/server/src/t4/claudeAccounts.ts +++ b/apps/server/src/t4/claudeAccounts.ts @@ -194,11 +194,9 @@ export const isClaudeAccountExhausted = ( }; /** - * The first account in order not used up for `model` (the main login included). With every one - * used up: the main login when it exists, else the account whose mark ends first. Claude then - * answers with its real limit, which T3's limit recovery marks and resumes at the reset, or runs - * if the mark was stale. A token-less "main" made Claude answer "Not logged in", a failed turn - * nothing resumes: usa-rs threads sat 17 h so on 2026-11-14 (AI-99). + * The first account not used up for `model` (the main login included); with none, the main login + * if it exists, else the account freeing first - so Claude answers with its real limit, never the + * "Not logged in" of a token-less main, which nothing resumes (AI-99). */ export const pickClaudeAccount = ( nowMs: number,