fix(ai): background generation cannot spend the interactive day

The automatic generators — status and reference cards, the briefing, the
arrival reaction, the nudge, document summaries and the index backfill —
reserved against the same ceiling as the chat. On the operator's account
that meant 150-330 ledger rows a day, 172 metric generations before
06:42Z, and a ceiling already spent when he opened the chat.

Every background surface now reserves through
`resolveDailyCapFor("job", chain)`: half the cost owner's ceiling, all
job surfaces together. The interactive routes keep the whole ceiling, so
a day of background work can no longer take the user's own away.
This commit is contained in:
Marc-André Bombeck committed 2026-09-11 10:09:34 +02:00
1 parent 2c46712e13
commit d3196ee69e
38 files changed
+151 -17

No files matched your search

@@ -35,6 +35,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
})),
reconcileSpend: vi.fn(async () => undefined),
resolveDailyCap: () => 200_000,
resolveDailyCapFor: () => 200_000,
resolveCostOwner: () => "operator" as const,
}));
@@ -51,6 +51,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn().mockResolvedValue({ allowed: true, reserved: 600 }),
reconcileSpend: vi.fn().mockResolvedValue(undefined),
resolveDailyCap: vi.fn(() => 100000),
resolveDailyCapFor: vi.fn(() => 100000),
resolveCostOwner: vi.fn(() => "operator" as const),
}));
vi.mock("@/lib/ai/provider-runner", () => ({
@@ -69,6 +69,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn().mockResolvedValue({ allowed: true, reserved: 1 }),
reconcileSpend: vi.fn().mockResolvedValue(undefined),
resolveDailyCap: vi.fn(() => 1000),
resolveDailyCapFor: vi.fn(() => 1000),
resolveCostOwner: vi.fn(() => "operator" as const),
}));
vi.mock("@/lib/documents/extract", () => ({
@@ -52,6 +52,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn().mockResolvedValue({ allowed: true, reserved: 1 }),
reconcileSpend: vi.fn().mockResolvedValue(undefined),
resolveDailyCap: vi.fn(() => 1000),
resolveDailyCapFor: vi.fn(() => 1000),
resolveCostOwner: vi.fn(() => "operator" as const),
}));
vi.mock("@/lib/modules/gate", () => ({
@@ -40,6 +40,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn().mockResolvedValue({ allowed: true, reserved: 1 }),
reconcileSpend: vi.fn().mockResolvedValue(undefined),
resolveDailyCap: vi.fn(() => 1000),
resolveDailyCapFor: vi.fn(() => 1000),
resolveCostOwner: vi.fn(() => "operator" as const),
}));
vi.mock("@/lib/modules/gate", () => ({
@@ -53,6 +53,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn().mockResolvedValue({ allowed: true, reserved: 1 }),
reconcileSpend: vi.fn().mockResolvedValue(undefined),
resolveDailyCap: vi.fn(() => 1000),
resolveDailyCapFor: vi.fn(() => 1000),
resolveCostOwner: vi.fn(() => "operator" as const),
}));
vi.mock("@/lib/modules/gate", () => ({
@@ -105,6 +105,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget,
reconcileSpend,
resolveDailyCap: vi.fn(() => 2_000_000),
resolveDailyCapFor: vi.fn(() => 2_000_000),
resolveCostOwner: vi.fn(() => "user" as const),
}));
@@ -86,6 +86,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn(async () => ({ allowed: true, reserved: 1500 })),
reconcileSpend: vi.fn(async () => undefined),
resolveDailyCap: vi.fn(() => 2_000_000),
resolveDailyCapFor: vi.fn(() => 2_000_000),
resolveCostOwner: vi.fn(() => "user" as const),
}));
vi.mock("@/lib/ai/coach/refusal", () => ({
@@ -134,6 +134,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget,
reconcileSpend,
resolveDailyCap: vi.fn(() => 2_000_000),
resolveDailyCapFor: vi.fn(() => 2_000_000),
resolveCostOwner: vi.fn(() => "user" as const),
}));
@@ -127,6 +127,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget,
reconcileSpend,
resolveDailyCap: vi.fn(() => 2_000_000),
resolveDailyCapFor: vi.fn(() => 2_000_000),
resolveCostOwner: vi.fn(() => "user" as const),
}));
@@ -109,6 +109,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget,
reconcileSpend,
resolveDailyCap: vi.fn(() => 2_000_000),
resolveDailyCapFor: vi.fn(() => 2_000_000),
resolveCostOwner: vi.fn(() => "user" as const),
}));
@@ -101,6 +101,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn(),
reconcileSpend: vi.fn(),
resolveDailyCap: vi.fn(() => 200_000),
resolveDailyCapFor: vi.fn(() => 200_000),
resolveCostOwner: vi.fn(() => "operator" as const),
}));
vi.mock("@/lib/ai/coach/refusal", () => ({ detectRefusal: vi.fn() }));
@@ -100,6 +100,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
})),
reconcileSpend: vi.fn(),
resolveDailyCap: vi.fn(() => 200_000),
resolveDailyCapFor: vi.fn(() => 200_000),
resolveCostOwner: vi.fn(() => "operator" as const),
}));
vi.mock("@/lib/ai/coach/refusal", () => ({ detectRefusal: vi.fn() }));
@@ -103,6 +103,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn(async () => ({ allowed: true, reserved: 1500 })),
reconcileSpend: vi.fn(async () => undefined),
resolveDailyCap: vi.fn(() => 2_000_000),
resolveDailyCapFor: vi.fn(() => 2_000_000),
resolveCostOwner: vi.fn(() => "user" as const),
}));
vi.mock("@/lib/ai/coach/refusal", () => ({
@@ -133,6 +133,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget,
reconcileSpend,
resolveDailyCap: vi.fn(() => 2_000_000),
resolveDailyCapFor: vi.fn(() => 2_000_000),
resolveCostOwner: vi.fn(() => "user" as const),
}));
@@ -177,6 +177,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn(async () => ({ allowed: true, reserved: 1500 })),
reconcileSpend: vi.fn(async () => undefined),
resolveDailyCap: vi.fn(() => 2_000_000),
resolveDailyCapFor: vi.fn(() => 2_000_000),
resolveCostOwner: vi.fn(() => "user" as const),
}));
vi.mock("@/lib/ai/coach/refusal", () => ({
@@ -45,6 +45,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
reserveBudget: vi.fn(),
reconcileSpend: vi.fn().mockResolvedValue(undefined),
resolveDailyCap: vi.fn(() => 200_000),
resolveDailyCapFor: vi.fn(() => 200_000),
resolveCostOwner: vi.fn(() => "operator" as const),
}));
vi.mock("@/lib/labs/ocr-extract", async () => {
@@ -44,6 +44,7 @@ vi.mock("@/lib/ai/coach/budget", () => ({
// returns a distinctive figure so an accidental default-cap regression is
// visible in the assertion rather than silently passing.
resolveDailyCap: vi.fn(() => 1_234_567),
resolveDailyCapFor: vi.fn(() => 1_234_567),
resolveCostOwner: vi.fn(() => "user" as const),
}));