From 426fd29f65b4cccf0b2bf5b00042d67edcfab6ae Mon Sep 17 00:00:00 2001 From: SawyerHood Date: Fri, 9 Oct 2026 17:31:09 +0000 Subject: [PATCH] Keep pooled affinity through the extended cache interval --- plugins/account-pool/src/hub.ts | 2 +- plugins/account-pool/src/server.test.ts | 77 +++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/plugins/account-pool/src/hub.ts b/plugins/account-pool/src/hub.ts index aa5057339a4..f5a3f44228d 100644 --- a/plugins/account-pool/src/hub.ts +++ b/plugins/account-pool/src/hub.ts @@ -54,7 +54,7 @@ const MAX_REFRESH_BACKOFFS = 1_024; const UPSTREAM_REJECTION_HOLD_MS = 60_000; const MAX_FAILURE_DETAIL_BYTES = 1_024; const FAILURE_DISPOSAL_TIMEOUT_MS = 250; -const AFFINITY_IDLE_TTL_MS = 30 * 60 * 1_000; +const AFFINITY_IDLE_TTL_MS = 65 * 60 * 1_000; const MAX_AFFINITY_BINDINGS = 4_096; const DROPPED_RESPONSE_HEADERS = new Set([ "content-encoding", diff --git a/plugins/account-pool/src/server.test.ts b/plugins/account-pool/src/server.test.ts index 673afb00231..2a3499c6134 100644 --- a/plugins/account-pool/src/server.test.ts +++ b/plugins/account-pool/src/server.test.ts @@ -3986,6 +3986,75 @@ describe("Account Pool plugin", () => { }, ); + it.each([ + { idleMinutes: 45, reload: false }, + { idleMinutes: 45, reload: true }, + { idleMinutes: 61, reload: false }, + { idleMinutes: 61, reload: true }, + ])( + "retains Claude session affinity after $idleMinutes idle minutes with reload=$reload", + async ({ idleMinutes, reload }) => { + let now = 1_800_000_000_000; + let outage = false; + const attempts: Array = []; + const upstreamFetch: typeof fetch = async (_input, init) => { + const key = new Headers(init?.headers).get("x-api-key"); + attempts.push(key); + return Response.json( + {}, + { status: outage && key === "sk-first" ? 503 : 200 }, + ); + }; + const fixture = await affinityFixture( + "claude", + upstreamFetch, + () => now, + ); + let host = fixture.host; + const send = async (id: string) => { + const response = await host.harness.behavior.fetchHttp( + "POST", + "/v1/messages", + { + headers: authHeaders(fixture.key), + body: claudeBody(id), + }, + ); + expect(response.status).toBe(200); + await response.text(); + return attempts.at(-1); + }; + expect(await send("original")).toBe("sk-first"); + outage = true; + expect(await send("fallback")).toBe("sk-second"); + outage = false; + now += idleMinutes * 60 * 1_000; + if (reload) { + host = await host.harness.lifecycle.reload( + createAccountPoolPlugin({ + fetch: upstreamFetch, + now: () => now, + usageUrl: EMPTY_USAGE_URL, + }), + ); + const service = host.harness.behavior.runService("hub"); + cleanups.push(async () => { + service.controller.abort(); + await service.done; + await host.harness.lifecycle.dispose(); + }); + await vi.waitFor(async () => { + const status = statusSchema.parse( + await host.harness.behavior.callRpc("status.get", null), + ); + expect(status.accepting).toBe(true); + }); + } + expect(await send("original")).toBe("sk-first"); + expect(await send("fresh")).toBe("sk-second"); + }, + ); + it.each(["claude", "codex"] as const)( "runs %s sequentially across conversations and keeps a recovered earlier account as backup", async (provider) => { @@ -4451,7 +4520,7 @@ describe("Account Pool plugin", () => { "account.disable", { id: fixture.account.id }, ); - if (reason === "expired") now += 31 * 60 * 1_000; + if (reason === "expired") now += 66 * 60 * 1_000; const key = reason === "other host" ? provider === "claude" @@ -4508,7 +4577,7 @@ describe("Account Pool plugin", () => { try { await movePoolToOtherAccount(fixture, provider); attempts.splice(1); - now += 29 * 60 * 1_000; + now += 64 * 60 * 1_000; await (await send("child-session", "parent-session")).text(); now += 2 * 60 * 1_000; await (await send("parent-session", null)).text(); @@ -4684,11 +4753,11 @@ describe("Account Pool plugin", () => { id: fixture.account.id, }); expect(await send(fixture.key, "{}", {})).toBe(keyFor("sk-second")); - now += 29 * 60 * 1_000; + now += 64 * 60 * 1_000; expect(await send(fixture.key, body)).toBe(keyFor("sk-first")); now += 2 * 60 * 1_000; expect(await send(fixture.key, body)).toBe(keyFor("sk-first")); - now += 31 * 60 * 1_000; + now += 66 * 60 * 1_000; expect(await send(fixture.key, body)).toBe(keyFor("sk-second")); } finally { await held.body?.cancel();