fix(ai): hold a reserve so the chat is never locked out by jobs

The background ceiling applied only when the operator funded the chain, so on a
chain the operator does not pay for the automatic generators and the chat shared
one undivided ceiling and the first could still take all of it. That is the
configuration the recommended remedy for the original lockout produces — put the
personal Codex account first — so the guarantee was missing on exactly the setup
it was written for.

The ceiling is now expressed as what it is meant to protect. Background work may
run until the day's spend would leave less than a fixed reserve for whoever is
waiting on an answer, on both cost owners. A reserve and a quota look alike at
the boundary and mean opposite things in the middle: a quota stops useful
generation on a day nobody was ever going to interrupt, while a reserve costs it
nothing until the moment it would take the last of the day. An account
generating 1.45 million tokens of insights on its own plan keeps every one of
them and the chat still holds 400 000.

INTERACTIVE_RESERVE_SHARE replaces JOB_SURFACE_SHARE. The old name described a
quota over the jobs rather than a floor under the person waiting, and a name
that states the opposite of the intent is what put the wrong comparison in this
ledger twice already.

The fraction is a default an operator will want to change. The setting and its
documentation are separate work.
This commit is contained in:
Marc-André Bombeck committed 2026-09-11 12:55:52 +02:00
1 parent b565edc48d
commit bcef5aeeee
6 files changed
+281 -74

No files matched your search

@@ -312,6 +312,98 @@ describe("coach_usage.operator_tokens (real Postgres)", () => {
expect(chat.allowed).toBe(true);
});
// v1.38.20 — the reserve holds on a chain nobody is billed for.
//
// This is the configuration the remedy for the original lockout produces:
// put the personal Codex account first and the operator stops paying for his
// own egress. The day then runs on `USER_PLAN_CAP` with no invoice behind it,
// so nothing rations the automatic generators — and without a reserve the
// chat is locked out by background work all over again, on the very chain
// that was supposed to fix it.
it("admits the chat on a user-funded chain whose background work filled the day", async () => {
const {
reserveBudget,
resolveDailyCapFor,
resolveDailyCap,
resolveCostOwner,
} = await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
const chain = [
{ providerType: "codex" as const },
{ providerType: "admin-openai" as const },
];
// 1.6 M tokens of insights on the user's own ChatGPT plan — everything the
// background surfaces are allowed to reach, and not a token of it billed.
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: 1_600_000,
operatorTokens: 0,
messageCount: 320,
},
});
const job = await reserveBudget(
userId,
3_000,
dateKey,
resolveDailyCapFor("job", chain),
resolveCostOwner(chain),
"job",
);
expect(job.allowed).toBe(false);
// Refused means refunded: the reserve is untouched by the attempt.
const afterJob = await readRow(userId, dateKey);
expect(afterJob?.totalTokens).toBe(1_600_000);
expect(afterJob?.messageCount).toBe(320);
const chat = await reserveBudget(
userId,
3_000,
dateKey,
resolveDailyCap(chain),
resolveCostOwner(chain),
"coach",
);
expect(chat.allowed).toBe(true);
expect(chat.limit).toBeNull();
});
it("lets that same background work run right up to the reserve", async () => {
// The reserve is a floor under the chat, not a quota over the jobs: a day
// of automatic generation on the user's own plan costs nobody anything and
// is not cut short until it would take the last of the day.
const { reserveBudget, resolveDailyCapFor, resolveCostOwner } =
await import("@/lib/ai/coach/budget");
const userId = await seedUser();
const dateKey = "2026-09-11";
const chain = [{ providerType: "codex" as const }];
await getPrismaClient().coachUsage.create({
data: {
userId,
dateKey,
totalTokens: 1_599_999,
operatorTokens: 0,
messageCount: 319,
},
});
const job = await reserveBudget(
userId,
3_000,
dateKey,
resolveDailyCapFor("job", chain),
resolveCostOwner(chain),
"job",
);
expect(job.allowed).toBe(true);
});
it("refunds both counters when a refused reservation is rolled back", async () => {
const { reserveBudget, OPERATOR_COST_CAP } =
await import("@/lib/ai/coach/budget");