mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
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:
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");
|
||||
|
||||
Reference in new issue
Block a user