docs(ai): drop planning vocabulary from the ledger comments

CLAUDE.md forbids planning shorthand in committed artefacts, and this branch
introduced 74 lines of it across source, tests, a schema field and two
migrations. Someone reading the migration SQL in a year has no way to resolve a
wave letter or a round number, and a comment that points at something outside
the repository names no reason at all.

Three passes. A version followed by a parenthetical keeps the version and loses
the parenthetical, because the version says since when something holds. A
sentence that leaned on the vocabulary to carry its meaning is rewritten to
state the reason directly: the arrival reservation now says it was refused over
money the operator never spent, rather than naming the defect a wave existed to
remove. Three test titles carried the marker in the name itself and are renamed
to what they pin.

Comments only. No behaviour, no renames beyond those three titles.
This commit is contained in:
Marc-André Bombeck committed 2026-09-11 12:37:54 +02:00
1 parent c66bcbc295
commit b565edc48d
30 files changed
+86 -84

No files matched your search

@@ -1,4 +1,4 @@
-- v1.38.19 (Wave E) — the daily coach ledger gains a cost-owner dimension.
-- v1.38.19 — the daily coach ledger gains a cost-owner dimension.
--
-- `coach_usage` counted one number per (user, day), so tokens the USER's own
-- plan paid for (a `codex` fallback hop after the operator's shared key
@@ -1,4 +1,4 @@
-- v1.38.19 (Wave E, fix round 1) — the arrival reaction remembers who paid for
-- v1.38.19 — the arrival reaction remembers who paid for
-- its reservation.
--
-- The row persisted the reserved amount and the date key but not the cost
+2 -2
View File
@@ -6271,7 +6271,7 @@ model CoachUsage {
userId String @map("user_id")
dateKey String @map("date_key")
totalTokens Int @default(0) @map("total_tokens")
/// v1.38.19 (Wave E) — the share of `totalTokens` served by a provider the
/// v1.38.19 — the share of `totalTokens` served by a provider the
/// OPERATOR funds (`admin-openai` / `admin-codex`). The operator-cost cap is
/// enforced against this counter and the user-plan cap against
/// `totalTokens`, so background work served by the user's own plan can no
@@ -6761,7 +6761,7 @@ model ArrivalReaction {
/// `coach_usage` increment. A reclaimed pre-provider attempt reuses it.
generationReservedTokens Int? @map("generation_reserved_tokens")
generationBudgetDateKey String? @map("generation_budget_date_key")
/// v1.38.19 (Wave E) — who the reservation above was booked to, `"operator"`
/// v1.38.19 — who the reservation above was booked to, `"operator"`
/// or `"user"`. Persisted rather than recomputed: the supersede refund in
/// `data-arrival.ts` has no chain to ask, and the resume path's chain may
/// have been reordered by the health ledger since the reservation was made.
@@ -2,7 +2,7 @@
* Structural guard — `coach_usage.total_tokens` and `coach_usage.operator_tokens`
* move together in EVERY raw statement.
*
* v1.38.19 (Wave E) gave the daily ledger a second counter: `operator_tokens`
* v1.38.19 gave the daily ledger a second counter: `operator_tokens`
* is the share of `total_tokens` an operator-funded provider served, and the
* operator ceiling is enforced against it. The counters are only meaningful
* while they stay in step. A statement that writes the total and forgets the
@@ -10,7 +10,8 @@
* reverses the total and forgets the operator share leaks budget permanently
* toward the operator cap, because nothing else ever subtracts it; a statement
* that RETURNS the total alone forces its caller to gate on the wrong number,
* which is the exact defect the wave exists to fix.
* which is how an operator-funded chain ends up refused over tokens the user's
* own plan paid for.
*
* Two of the three writers shipped with precisely those bugs, and both were a
* grep away. This guard is that grep, executable: every INSERT column list,
@@ -207,7 +207,7 @@ describe("GET /api/admin/provider-health", () => {
});
/**
* v1.38.19 (Wave E) — the two spend figures side by side.
* v1.38.19 — the two spend figures side by side.
*
* The operator reads this card when the chat refuses. Until now it showed
* delivery health only, and the day's token figure was one number that mixed
+1 -1
View File
@@ -111,7 +111,7 @@ export const GET = apiHandler(async () => {
return a.providerType.localeCompare(b.providerType);
});
// v1.38.19 (Wave E) — the day's spend, split by who pays for it.
// v1.38.19 — the day's spend, split by who pays for it.
//
// The operator opens this card when a surface refuses on budget. One mixed
// number could not answer the question it raised: a day at 1.24 M tokens is
+2 -2
View File
@@ -609,7 +609,7 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
"coach",
);
if (!reservation.allowed) {
// v1.38.19 (Wave E, fix round 1) — say WHICH ceiling refused and WHOSE.
// v1.38.19 — say WHICH ceiling refused and WHOSE.
// `totalAfter` alone is the day's mixed total; on an operator refusal that
// is not the counter that tripped, and reading `totalAfter: 1200000`
// against a 200 000 ceiling is what turned the 06:42Z incident into a
@@ -654,7 +654,7 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
async function produceReply(): Promise<ReplyOutcome> {
let result: CompletionResult;
// v1.38.19 (Wave E) — typed as the chain's provider union, not a bare
// v1.38.19 — typed as the chain's provider union, not a bare
// string: the budget reconcile attributes the turn's tokens to the cost
// owner of the hop named here.
let workingProviderType: ProviderChainType;
@@ -120,9 +120,9 @@ describe("POST /api/labs/ocr/extract — text mode budget", () => {
"2026-06-26",
// F1 — the provider-aware daily cap (mocked) is threaded as the 4th arg.
200_000,
// Wave E — and the cost owner that cap is enforced against as the 5th.
// ...the cost owner that cap is enforced against as the 5th...
"operator",
// Wave E fix round 1 — the surface the ceiling is rationed for, 6th.
// ...and the surface the ceiling is rationed for as the 6th.
"coach",
);
});
@@ -4,7 +4,7 @@ import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
/**
* v1.38.19 (Wave E) — the day's spend, split by who paid for it.
* v1.38.19 — the day's spend, split by who paid for it.
*
* `/api/admin/provider-health` answers `spendToday` and nothing rendered it,
* which is the "the reader is the follow-up" shape the two-ended rule exists to
@@ -27,7 +27,7 @@ import { formatDateTime } from "@/lib/format";
*/
/**
* v1.38.19 (Wave E) — the day's spend, split by who pays for it. The operator
* v1.38.19 — the day's spend, split by who pays for it. The operator
* opens this card when a surface refuses on budget, and one mixed number could
* not answer the question that raises: a day at 1.24 M tokens is alarming until
* you see that 151 200 of them were the instance's own key and the rest ran on
+2 -2
View File
@@ -774,7 +774,7 @@ describe("runRawCompletionWithFallback — operator-cost cap at hop time", () =>
expect(adminOpenai.callCount).toBe(1);
});
// v1.38.19 (Wave E) — the guard reads the OPERATOR-funded share.
// v1.38.19 — the guard reads the OPERATOR-funded share.
//
// Production evidence (2026-09-11): the operator's day held ~1.2 M tokens,
// nearly all served by `codex` on his own ChatGPT plan. Comparing the day's
@@ -833,7 +833,7 @@ describe("runRawCompletionWithFallback — operator-cost cap at hop time", () =>
expect(adminOpenai.callCount).toBe(0);
});
// v1.38.19 (Wave E, fix round 1) — the hop guard knows which SURFACE asked.
// v1.38.19 — the hop guard knows which SURFACE asked.
//
// Without this the job share was enforced at reservation time only: a
// background reservation admitted under `resolveDailyCapFor("job", …)` could
+6 -6
View File
@@ -198,7 +198,7 @@ describe("reserveBudget cap (F1 — user-plan path not locked out)", () => {
});
/**
* v1.38.19 (Wave E) — the cap is enforced against the counter it is ABOUT.
* v1.38.19 — the cap is enforced against the counter it is ABOUT.
*
* Production evidence (2026-09-11): the operator's day held 1.0–1.45 M tokens,
* almost all served by `codex` on his own ChatGPT plan after the shared
@@ -207,14 +207,14 @@ describe("reserveBudget cap (F1 — user-plan path not locked out)", () => {
* is the defect; the operator ceiling may only see operator-funded tokens.
*/
/**
* v1.38.19 (Wave E) — background generation gets a bounded slice of the day.
* v1.38.19 — background generation gets a bounded slice of the day.
*
* Production evidence (2026-09-11): 150–330 ledger rows a day on the
* operator's account, 172 `insights.metric` generations by 06:42Z — the
* automatic jobs had spent the day's ceiling before he opened the chat. A
* background surface may reserve at most half of the interactive ceiling.
*/
describe("resolveDailyCapFor (Wave E — the job share)", () => {
describe("resolveDailyCapFor — the job share", () => {
it("gives a background surface half the operator ceiling", () => {
expect(resolveDailyCapFor("job", [{ providerType: "admin-openai" }])).toBe(
OPERATOR_COST_CAP * JOB_SURFACE_SHARE,
@@ -247,7 +247,7 @@ describe("resolveDailyCapFor (Wave E — the job share)", () => {
});
});
describe("resolveTotalCapFor (Wave E, fix round 1 — the abuse ceiling)", () => {
describe("resolveTotalCapFor — the abuse ceiling", () => {
it("keeps the user-plan ceiling on the day's mixed total for both owners", () => {
// The operator arm compares `operator_tokens`, a counter that returns to
// ~0 on every reconcile the user's own plan settled. Without a ceiling on
@@ -267,7 +267,7 @@ describe("resolveTotalCapFor (Wave E, fix round 1 — the abuse ceiling)", () =>
});
});
describe("reserveBudget cap by cost owner (Wave E)", () => {
describe("reserveBudget — the cap follows the cost owner", () => {
let prismaMock: {
$queryRaw: ReturnType<typeof vi.fn>;
$executeRaw: ReturnType<typeof vi.fn>;
@@ -336,7 +336,7 @@ describe("reserveBudget cap by cost owner (Wave E)", () => {
expect(res.allowed).toBe(false);
});
// v1.38.19 (Wave E, fix round 1) — the abuse ceiling survives the owner split.
// v1.38.19 — the abuse ceiling survives the owner split.
//
// The first cut compared `operator_tokens` and NOTHING else for an
// operator-funded chain. On the 2026-09-11 shape that counter never grows:
+15 -15
View File
@@ -18,7 +18,7 @@ import { prisma } from "@/lib/db";
import type { ProviderChainType } from "@/lib/ai/provider-chain";
/**
* v1.38.19 (Wave E, fix round 1) — the two raw statements the ledger owns can
* v1.38.19 — the two raw statements the ledger owns can
* run on the singleton client OR inside an open transaction. The arrival
* reaction reserves its tokens in the same transaction that links the
* reservation to the claimed marker, and used to carry its own copy of the
@@ -79,7 +79,7 @@ export function resolveDailyCap(
}
/**
* v1.38.19 (Wave E) — the share of a day's ceiling that BACKGROUND generation
* v1.38.19 — the share of a day's ceiling that BACKGROUND generation
* may reserve, all job surfaces together.
*
* Production evidence (2026-09-11): the operator's account wrote 150–330
@@ -91,7 +91,7 @@ export function resolveDailyCap(
export const JOB_SURFACE_SHARE = 0.5;
/**
* v1.38.19 (Wave E) — which kind of caller is asking. `"coach"` is every
* v1.38.19 — which kind of caller is asking. `"coach"` is every
* interactive surface (the chat, the extraction routes, the document routes,
* the connection probe): a person is waiting. `"job"` is the automatic
* generators, which run with nobody waiting and must leave the interactive
@@ -100,7 +100,7 @@ export const JOB_SURFACE_SHARE = 0.5;
export type BudgetSurface = "coach" | "job";
/**
* v1.38.19 (Wave E, fix round 1) — the job share rations the OPERATOR's
* v1.38.19 — the job share rations the OPERATOR's
* invoice, so it applies only when the operator is paying.
*
* The first cut multiplied whichever ceiling `resolveDailyCap` returned, which
@@ -120,7 +120,7 @@ function applyJobShare(
}
/**
* v1.38.19 (Wave E) — the daily ceiling for one SURFACE on a chain. `"coach"`
* v1.38.19 — the daily ceiling for one SURFACE on a chain. `"coach"`
* (every interactive surface: the chat, the extraction routes, the document
* routes, the connection probe) gets the whole ceiling; `"job"` (the automatic
* generators) gets `JOB_SURFACE_SHARE` of it. The cost owner is unchanged —
@@ -135,7 +135,7 @@ export function resolveDailyCapFor(
}
/**
* v1.38.19 (Wave E, fix round 1) — the ABUSE ceiling on the day's mixed total,
* v1.38.19 — the ABUSE ceiling on the day's mixed total,
* for one surface and one cost owner.
*
* `resolveDailyCapFor` answers "how much of the owner's ceiling may this
@@ -169,14 +169,14 @@ export function isOperatorFundedProvider(type: ProviderChainType): boolean {
}
/**
* v1.38.19 (Wave E) — who pays for a turn. `"operator"` means the tokens land
* v1.38.19 — who pays for a turn. `"operator"` means the tokens land
* on the operator's invoice (`admin-openai` / `admin-codex`), `"user"` means
* the user's own plan, key or hardware carries them.
*/
export type BudgetCostOwner = "operator" | "user";
/**
* v1.38.19 (Wave E) — the cost owner a chain RESERVES under: the primary is
* v1.38.19 — the cost owner a chain RESERVES under: the primary is
* the provider that will be tried first, so it is the expected payer. An empty
* chain defaults to the operator — the conservative side. `resolveDailyCap`
* and `reserveBudget` must agree on this classification, so both read it here.
@@ -204,7 +204,7 @@ export interface DailySpend {
/** Every token recorded for the day, whoever paid for it. */
total: number;
/**
* v1.38.19 (Wave E) — the share of `total` served by an operator-funded
* v1.38.19 — the share of `total` served by an operator-funded
* provider. The hop guard compares THIS against `OPERATOR_COST_CAP`: a day
* full of turns the user's own plan paid for must not close the operator's
* fallback hop.
@@ -264,7 +264,7 @@ export interface ReserveBudgetResult {
/** The day's total AFTER this reservation (for observability). */
totalAfter: number;
/**
* v1.38.19 (Wave E) — the owner this reservation was charged to. The
* v1.38.19 — the owner this reservation was charged to. The
* reconcile needs it: only a reservation booked to the operator has an
* amount to move back OUT of `operator_tokens` when a user-funded hop ends
* up serving the turn.
@@ -273,7 +273,7 @@ export interface ReserveBudgetResult {
/** The day's operator-funded spend AFTER this reservation. */
operatorAfter: number;
/**
* v1.38.19 (Wave E, fix round 1) — which ceiling refused this reservation,
* v1.38.19 — which ceiling refused this reservation,
* `null` when it was admitted. `"owner-cap"` is the cost owner's daily
* ceiling (the operator's invoice, or the user's own plan); `"total-cap"` is
* the abuse ceiling on the day's mixed total. The refusal annotations carry
@@ -298,7 +298,7 @@ export async function reserveBudget(
Number.isFinite(estimatedTokens) && estimatedTokens > 0
? Math.floor(estimatedTokens)
: 0;
// v1.38.19 (Wave E) — a reservation is booked to `operator_tokens` only when
// v1.38.19 — a reservation is booked to `operator_tokens` only when
// the chain's primary is operator-funded. The counter moves inside the SAME
// statement as the total, so two concurrent requests can never observe one
// counter without the other.
@@ -328,14 +328,14 @@ export async function reserveBudget(
// PRIOR to its reservation was under the cap. So compare the post-increment
// figure minus this reservation against the cap.
//
// v1.38.19 (Wave E) — and compare the counter the cap is ABOUT. An
// v1.38.19 — and compare the counter the cap is ABOUT. An
// operator-funded chain is measured against the day's operator-funded spend,
// never against a total that the user's own plan inflated; a user-plan chain
// keeps its abuse ceiling on the total.
const priorTotal = totalAfter - reserved;
const priorOperator = operatorAfter - operatorReserved;
const prior = owner === "operator" ? priorOperator : priorTotal;
// v1.38.19 (Wave E, fix round 1) — BOTH ceilings, always. The owner's cap
// v1.38.19 — BOTH ceilings, always. The owner's cap
// protects whoever pays; the total cap is the abuse ceiling that keeps a
// runaway loop — or a day of background generation the operator's key never
// served — from writing an unbounded row. Dropping the second one for
@@ -412,7 +412,7 @@ export async function reconcileSpend(
// (shouldn't happen, but the wire is untrusted) can't drive a negative charge.
const actual = Math.max(0, grossActual - cached);
const delta = actual - reserved;
// v1.38.19 (Wave E) — settle the operator's counter against the hop that
// v1.38.19 — settle the operator's counter against the hop that
// SERVED, not the one the chain expected. Book the actual tokens when an
// operator-funded provider answered, and take back whatever this
// reservation had put there (nothing, when the reservation was the user's
+3 -3
View File
@@ -438,11 +438,11 @@ async function runRawChain(
total: 0,
operator: 0,
}));
// v1.38.19 (Wave E) — the OPERATOR-funded share, not the day's total: a
// v1.38.19 — the OPERATOR-funded share, not the day's total: a
// day filled by the user's own plan must not close the operator's
// fallback hop on money the operator never spent.
//
// v1.38.19 (Wave E, fix round 1) — and at the SURFACE's ceiling, not the
// v1.38.19 — and at the SURFACE's ceiling, not the
// full one. A background reservation is rationed at the job share when it
// is taken; without this the hop it reaches by fallback could still spend
// against the whole operator cap, which is how the original defect stayed
@@ -540,7 +540,7 @@ export async function runStreamingRawCompletionWithFallback(args: {
providers: ProviderChainResolved[];
params: CompletionParams;
/**
* v1.38.19 (Wave E, fix round 1) — which ceiling an operator-funded FALLBACK
* v1.38.19 — which ceiling an operator-funded FALLBACK
* hop is rationed against. `"job"` gets the background share, exactly as the
* reservation did; `"coach"` gets the whole ceiling.
*/
+1 -1
View File
@@ -278,7 +278,7 @@ export async function maybeAutoStageLabFacts(
// A text-structuring pass, not a vision call — reserve the proportionate
// ceiling under the cost owner the auto-index resolved.
// v1.38.19 (Wave E) — auto-staging runs off the upload, with no one waiting
// v1.38.19 — auto-staging runs off the upload, with no one waiting
// on it: a background surface, so half the day's ceiling.
const dateKey = buildDateKey();
const reservation = await reserveBudget(
+2 -2
View File
@@ -131,7 +131,7 @@ export interface ResolvedIndexProvider {
consentOk: boolean;
dailyCap: number;
/**
* v1.38.19 (Wave E) — whose budget `dailyCap` rations. The reservation books
* v1.38.19 — whose budget `dailyCap` rations. The reservation books
* `operator_tokens` only for an operator-funded pick, so the cap and the
* counter it is compared against are resolved from one place.
*/
@@ -168,7 +168,7 @@ export async function resolveIndexProvider(
pick && consentOk
? resolveDailyCap([{ providerType: pick.entry.providerType }])
: 0;
// v1.38.19 (Wave E) — the cap and its cost owner are resolved from the SAME
// v1.38.19 — the cap and its cost owner are resolved from the SAME
// pick, so the reservation books the counter the cap is enforced against.
const costOwner: BudgetCostOwner =
pick && consentOk
@@ -21,7 +21,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
/** In-memory stand-in for the day's `coach_usage.total_tokens`. */
let ledgerTotal = 0;
/** …and of `coach_usage.operator_tokens` (Wave E). */
/** …and of `coach_usage.operator_tokens`. */
let ledgerOperator = 0;
vi.mock("@/lib/db", () => ({
@@ -30,7 +30,7 @@ vi.mock("@/lib/db", () => ({
// (strings, userId, dateKey, reserved, reserved).
$queryRaw: vi.fn(
async (_strings: TemplateStringsArray, ...v: unknown[]) => {
// Wave E — (userId, dateKey, reserved, operatorReserved, …).
// (userId, dateKey, reserved, operatorReserved, …).
ledgerTotal += Number(v[2] ?? 0);
ledgerOperator += Number(v[3] ?? 0);
return [{ total_tokens: ledgerTotal, operator_tokens: ledgerOperator }];
@@ -41,7 +41,7 @@ vi.mock("@/lib/db", () => ({
async (strings: TemplateStringsArray, ...v: unknown[]) => {
const sql = strings.join("?");
const amount = Number(v[0] ?? 0);
// Wave E — the operator-funded share moves in the same statement.
// The operator-funded share moves in the same statement.
const operatorAmount = Number(v[1] ?? 0);
if (sql.includes("total_tokens + ")) {
ledgerTotal += amount;
@@ -15,7 +15,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
let ledgerTotal = 0;
/** …and of `coach_usage.operator_tokens` (Wave E). */
/** …and of `coach_usage.operator_tokens`. */
let ledgerOperator = 0;
const findUnique = vi.fn();
@@ -31,7 +31,7 @@ vi.mock("@/lib/db", () => ({
prisma: {
$queryRaw: vi.fn(
async (_strings: TemplateStringsArray, ...v: unknown[]) => {
// Wave E — (userId, dateKey, reserved, operatorReserved, …).
// (userId, dateKey, reserved, operatorReserved, …).
ledgerTotal += Number(v[2] ?? 0);
ledgerOperator += Number(v[3] ?? 0);
return [{ total_tokens: ledgerTotal, operator_tokens: ledgerOperator }];
@@ -41,7 +41,7 @@ vi.mock("@/lib/db", () => ({
async (strings: TemplateStringsArray, ...v: unknown[]) => {
const sql = strings.join("?");
const amount = Number(v[0] ?? 0);
// Wave E — the operator-funded share moves in the same statement.
// The operator-funded share moves in the same statement.
const operatorAmount = Number(v[1] ?? 0);
if (sql.includes("total_tokens + ")) {
ledgerTotal += amount;
@@ -18,7 +18,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
/** In-memory stand-in for the day's `coach_usage.total_tokens`. */
let ledgerTotal = 0;
/** …and of `coach_usage.operator_tokens` (Wave E). */
/** …and of `coach_usage.operator_tokens`. */
let ledgerOperator = 0;
vi.mock("@/lib/db", () => ({
@@ -27,7 +27,7 @@ vi.mock("@/lib/db", () => ({
// (strings, userId, dateKey, reserved, reserved).
$queryRaw: vi.fn(
async (_strings: TemplateStringsArray, ...v: unknown[]) => {
// Wave E — (userId, dateKey, reserved, operatorReserved, …).
// (userId, dateKey, reserved, operatorReserved, …).
ledgerTotal += Number(v[2] ?? 0);
ledgerOperator += Number(v[3] ?? 0);
return [{ total_tokens: ledgerTotal, operator_tokens: ledgerOperator }];
@@ -38,7 +38,7 @@ vi.mock("@/lib/db", () => ({
async (strings: TemplateStringsArray, ...v: unknown[]) => {
const sql = strings.join("?");
const amount = Number(v[0] ?? 0);
// Wave E — the operator-funded share moves in the same statement.
// The operator-funded share moves in the same statement.
const operatorAmount = Number(v[1] ?? 0);
if (sql.includes("total_tokens + ")) {
ledgerTotal += amount;
@@ -129,7 +129,7 @@ describe("runStatusCompletion — ledger accounting", () => {
});
it("refuses a background generation at the job share while the chat still runs", async () => {
// Wave E — the automatic status/reference generators may spend at most
// The automatic status/reference generators may spend at most
// half the operator ceiling, so the interactive chat still has a day left
// when the jobs have had theirs. One token past the job share:
ledgerTotal = 100_001;
@@ -177,7 +177,7 @@ describe("runStatusCompletion — ledger accounting", () => {
// "no provider configured" assessment, which a budget refusal is not.
expect(runRawCompletionWithFallback).not.toHaveBeenCalled();
expect(result.kind).toBe("error");
// v1.38.19 (Wave E, fix round 1) — the refusal has to SAY which ceiling it
// v1.38.19 — the refusal has to SAY which ceiling it
// hit and whose. `{ totalAfter }` alone was the day's mixed total, which on
// an operator refusal is not the counter that tripped; and the shared
// `error` outcome cannot distinguish an exhausted background share from a
+1 -1
View File
@@ -118,7 +118,7 @@ export async function runBriefingCompletion(
args.maxTokens +
Math.ceil((args.systemPrompt.length + args.userPrompt.length) / 4);
// v1.38.19 (Wave E) — a background surface: half the day's ceiling when the
// v1.38.19 — a background surface: half the day's ceiling when the
// operator funds the chain.
const jobCap = resolveDailyCapFor("job", args.chain);
const reservation = await reserveBudget(
+2 -2
View File
@@ -219,7 +219,7 @@ export async function runStatusCompletion(
const dateKey = buildDateKey();
const estimatedTokens =
maxTokens + Math.ceil((systemPrompt.length + userPrompt.length) / 4);
// v1.38.19 (Wave E) — a background surface: half the day's ceiling when the
// v1.38.19 — a background surface: half the day's ceiling when the
// operator funds the chain.
const jobCap = resolveDailyCapFor("job", chain);
const reservation = await reserveBudget(
@@ -236,7 +236,7 @@ export async function runStatusCompletion(
// which callers cache as the settled "no provider configured" assessment.
// The distinct annotation keeps the refusal observable even though the
// result shape is shared.
// v1.38.19 (Wave E, fix round 1) — the shared `error` outcome cannot say
// v1.38.19 — the shared `error` outcome cannot say
// WHY the generation stopped, so the annotation has to. `surface: "job"`
// plus `cap` separates "this job used up the background share" from "the
// whole day is spent", and `limit` says which counter tripped. The operator
@@ -199,7 +199,7 @@ beforeEach(async () => {
isModuleEnabled.mockResolvedValue(true);
chainRequiresServerManagedConsent.mockReturnValue(false);
hasActiveConsentForSurface.mockResolvedValue(true);
// v1.38.19 (Wave E, fix round 1) — the claim reservation is no longer a
// v1.38.19 — the claim reservation is no longer a
// second copy of the upsert; it calls the shared `reserveBudget` with the
// transaction client. The spy therefore runs the REAL gate against the
// mocked `$queryRaw`, so the rows a test hands back are the ledger the gate
@@ -525,12 +525,12 @@ describe("reaction line — degradation", () => {
});
it("a day the user's own plan paid for still admits the reaction", async () => {
// The 2026-09-11 shape, on the surface the wave forgot to convert: the
// operator's shared key answered 500 all morning and `codex` — his own
// ChatGPT plan — served everything, so the day's total is large and the
// operator's counter is empty. The reservation used to compare that total
// against the operator's ceiling and refuse, which is the exact defect the
// wave exists to remove, reproduced at HALF the old ceiling.
// The 2026-09-11 shape: the operator's shared key answered 500 all
// morning and `codex` — his own ChatGPT plan — served everything, so the
// day's total is large and the operator's counter is empty. The
// reservation used to compare that total against the operator's ceiling
// and refuse, locking the surface out over money the operator never spent
// — and at HALF the old ceiling, because this is a background surface.
resolveProviderChain.mockResolvedValue([
{
providerType: "openai",
@@ -744,7 +744,7 @@ describe("reaction line — degradation", () => {
});
/**
* v1.38.19 (Wave E, fix round 1) — a reservation is never stranded.
* v1.38.19 — a reservation is never stranded.
*
* Every path that crosses the provider boundary has to settle the ledger, and
* settle it against the owner the reservation was actually booked to. Nothing
+1 -1
View File
@@ -191,7 +191,7 @@ export const composeNudgeWithAI: ComposeNudgeWithAI = async (params) => {
const budget = AI_BUDGETS.coachNudge;
const maxTokens = budget.maxTokens ?? 160;
// v1.38.19 (Wave E) — a background surface: half the day's ceiling.
// v1.38.19 — a background surface: half the day's ceiling.
const dailyCap = resolveDailyCapFor("job", chain);
const dateKey = buildDateKey();
+2 -2
View File
@@ -62,7 +62,7 @@ interface LockedReaction {
generationReservedTokens: number | null;
generationBudgetDateKey: string | null;
/**
* v1.38.19 (Wave E) — `"operator"` when the reservation below was booked to
* v1.38.19 — `"operator"` when the reservation below was booked to
* the operator's counter, `"user"` otherwise, null on a row reserved before
* the column existed. The refund has to reverse the SAME counters the
* reservation moved, and there is no chain here to ask.
@@ -125,7 +125,7 @@ async function claimReaction(
current.generationBudgetDateKey != null &&
current.generationProviderInvokedAt == null
) {
// v1.38.19 (Wave E, fix round 1) — reverse BOTH counters, or neither.
// v1.38.19 — reverse BOTH counters, or neither.
//
// The reservation booked `operator_tokens` too whenever the chain's
// primary was operator-funded. Refunding only the total left that share
@@ -200,7 +200,7 @@ export async function runContentIndexBackfillForUser(
// leaves every PDF out of "index all documents".
const candidateMimes = [...IMAGE_MIMES, "application/pdf"];
// v1.38.19 (Wave E) — a background surface: half the day's ceiling.
// v1.38.19 — a background surface: half the day's ceiling.
const dailyCap = resolveDailyCapFor("job", [
{ providerType: pick.entry.providerType },
]);
+1 -1
View File
@@ -191,7 +191,7 @@ export async function runDocumentSummaryJob(
userId,
AI_BUDGETS.documentSummary.maxTokens,
dateKey,
// v1.38.19 (Wave E) — a background surface: half the day's ceiling.
// v1.38.19 — a background surface: half the day's ceiling.
resolveDailyCapFor("job", [{ providerType: pick.entry.providerType }]),
resolveCostOwner([{ providerType: pick.entry.providerType }]),
"job",
+8 -8
View File
@@ -318,7 +318,7 @@ type ReactionReservation = {
reserved: number;
dateKey: string;
/**
* v1.38.19 (Wave E) — the cost owner the reservation was booked to. It is
* v1.38.19 — the cost owner the reservation was booked to. It is
* PERSISTED on the row (`generationCostOwner`), not recomputed: the chain is
* re-resolved per run and the health ledger reorders it, so a resumed
* reservation that asked the chain again could reverse a counter the
@@ -363,15 +363,15 @@ async function reserveClaimBudget(
});
if (stillOwned.count !== 1) throw new ReactionClaimLostError();
// v1.38.19 (Wave E, fix round 1) — ONE reservation implementation.
// v1.38.19 — ONE reservation implementation.
//
// This surface used to carry its own copy of the upsert so the write could
// ride the claim transaction. The copy drifted: it learned to WRITE
// `operator_tokens` and kept gating on the day's mixed total, at the
// halved job ceiling — so on the operator's own 2026-09-11 row (1.2 M
// total, ~0 operator) the arrival reaction was refused all day, which is
// the exact defect this wave exists to remove, reproduced on the one
// surface the wave forgot to convert. `reserveBudget` takes the
// halved job ceiling — so on a row holding 1.2 M tokens of which almost
// none were the operator's (2026-09-11, the shared key answering 500 while
// the user's own plan served), the arrival reaction was refused all day
// over money the operator never spent. `reserveBudget` takes the
// transaction client instead, so both writes still roll back together and
// there is no second copy left to drift.
const reservation = await reserveBudget(
@@ -406,7 +406,7 @@ async function reserveClaimBudget(
data: {
generationReservedTokens: reserved,
generationBudgetDateKey: dateKey,
// v1.38.19 (Wave E, fix round 1) — persist the cost owner with the
// v1.38.19 — persist the cost owner with the
// amount. Two readers need it and neither can derive it: the resume
// path below (the chain may have been reordered since) and the
// supersede refund in `data-arrival.ts` (which has no chain at all).
@@ -562,7 +562,7 @@ export async function runReactionLine(
job.userId,
revision,
buildDateKey(),
// v1.38.19 (Wave E) — a background surface: half the day's ceiling.
// v1.38.19 — a background surface: half the day's ceiling.
resolveDailyCapFor("job", chain),
resolveCostOwner(chain),
);
@@ -266,7 +266,7 @@ describe("ArrivalReaction — the day claim", () => {
expect(usage).toMatchObject({ totalTokens: 280, messageCount: 1 });
});
// v1.38.19 (Wave E, fix round 1) — the refund reverses BOTH counters.
// v1.38.19 — the refund reverses BOTH counters.
//
// The reservation booked `operator_tokens` whenever the chain's primary was
// operator-funded. The supersede refund reversed only the total, and nothing
+5 -4
View File
@@ -1,5 +1,5 @@
/**
* v1.38.19 (Wave E) — the daily coach ledger attributes tokens to the COST
* v1.38.19 — the daily coach ledger attributes tokens to the COST
* OWNER of the hop that actually served them.
*
* Production evidence (operator instance, 2026-09-11): the chat refused with
@@ -219,12 +219,13 @@ describe("coach_usage.operator_tokens (real Postgres)", () => {
expect(row?.operatorTokens).toBe(OPERATOR_COST_CAP);
});
// v1.38.19 (Wave E, fix round 1) — the incident, end to end.
// v1.38.19 — the incident, end to end.
//
// The 06:42Z row verbatim: 1.2 M tokens on the day, none of them the
// operator's, on the chain `[admin-openai, codex, openai-compatible]`. The
// chat must open. Before the wave it was refused because the ledger had no
// owner dimension; this pins that the refusal cannot come back.
// chat must open. It was refused while the ledger had no owner dimension and
// counted every token against the operator's ceiling; this pins that the
// refusal cannot come back.
it("does not refuse the chat for a day of background spend on the user's own plan", async () => {
const { reserveBudget, resolveDailyCap, resolveCostOwner } =
await import("@/lib/ai/coach/budget");
@@ -38,7 +38,7 @@ describe("reserveBudget (real Postgres)", () => {
// Pre-spend 900 so a single 600-token reservation stays under the cap
// but two concurrent ones cannot both be admitted.
//
// v1.38.19 (Wave E) — the 900 sits on BOTH counters. The ledger grew a cost
// v1.38.19 — the 900 sits on BOTH counters. The ledger grew a cost
// dimension: an operator-owned reservation is measured against
// `operator_tokens`, so a fixture that pre-spends only the mixed total no
// longer states its own premise ("900 already spent under this cap") and