/**
* Claude Code's `N tokens left` signal, pure half.
*
* What the number is (read off a live Claude Code session and the 2.1.261
* capture, findings §14): a PER-TURN token budget, not the context window. It
* reads 15,000,000 at every turn start — the last line of the system prompt
* before the gitStatus block, and the end of every user message — and counts
* down on the tool results within the turn by roughly what each round added
* (tool results, assistant output); the next user message resets it. Constant
* at the cached-prefix positions, variable only on the tail, so it never busts
* the prefix. One Code approximates the countdown as the growth of the context
* since the turn started (pi's context-usage estimate), the same quantity to
* within the estimator's error. `CC_TOTAL_TOKENS_BUDGET` overrides the figure.
*/
/** Claude Code's per-turn budget as observed; the meaning of the figure is CC's max tokens per turn with compaction. */
export const DEFAULT_TURN_TOKEN_BUDGET = 15_000_000;
/** The configured budget: `CC_TOTAL_TOKENS_BUDGET` when it is a positive number, else Claude Code's default. */
export function turnTokenBudget(env: Record = process.env): number {
const raw = env.CC_TOTAL_TOKENS_BUDGET;
const n = raw === undefined || raw.trim() === "" ? Number.NaN : Number(raw);
return Number.isFinite(n) && n > 0 ? Math.round(n) : DEFAULT_TURN_TOKEN_BUDGET;
}
function finite(n: number | null | undefined): number | undefined {
return typeof n === "number" && Number.isFinite(n) ? n : undefined;
}
/**
* One turn's countdown. `startTurn` takes the context size the turn begins
* from; `left` takes the size now and returns what remains of the budget. The
* value never rises within a turn (as CC's does not), the first known size
* becomes the baseline when the turn started without an estimate, and an
* unknown size repeats the last value.
*/
export class TurnBudget {
private start: number | undefined;
private spent = 0;
constructor(readonly budget: number) {}
startTurn(contextTokens: number | null | undefined): void {
this.start = finite(contextTokens);
this.spent = 0;
}
left(contextTokens: number | null | undefined): number {
const now = finite(contextTokens);
if (now !== undefined) {
if (this.start === undefined) this.start = now;
this.spent = Math.max(this.spent, now - this.start);
}
return Math.max(0, this.budget - this.spent);
}
}
/** The bare block Claude Code appends, byte-for-byte. */
export function totalTokensBlock(left: number): string {
return `${left} tokens left`;
}