Why this matters
Claude Code has to decide whether a turn should keep going or switch into cleanup work.
Data structures you need first
BudgetTracker
This tracker remembers what the continuation logic has already seen.
export type BudgetTracker = {
continuationCount: number
lastDeltaTokens: number
lastGlobalTurnTokens: number
startedAt: number
}| Field | Meaning | First Pass |
|---|---|---|
continuationCount | How many continuation nudges have already happened. | Very important |
lastDeltaTokens | How many tokens were gained since the prior check. | Important |
lastGlobalTurnTokens | The prior total token count remembered by the tracker before the next budget check. | Helpful once you compare iterations |
startedAt | When the budget tracking window began. | Safe to skim |
Code walk
The budget gate returns either continue or stop:
const COMPLETION_THRESHOLD = 0.9
const DIMINISHING_THRESHOLD = 500
export function checkTokenBudget(
tracker: BudgetTracker,
agentId: string | undefined,
budget: number | null,
globalTurnTokens: number,
): TokenBudgetDecision {
if (agentId || budget === null || budget <= 0) {
return { action: 'stop', completionEvent: null }
}
const turnTokens = globalTurnTokens
const pct = Math.round((turnTokens / budget) * 100)
const deltaSinceLastCheck = globalTurnTokens - tracker.lastGlobalTurnTokens
const isDiminishing =
tracker.continuationCount >= 3 &&
deltaSinceLastCheck < DIMINISHING_THRESHOLD &&
tracker.lastDeltaTokens < DIMINISHING_THRESHOLD
if (!isDiminishing && turnTokens < budget * COMPLETION_THRESHOLD) {
tracker.continuationCount++
tracker.lastDeltaTokens = deltaSinceLastCheck
tracker.lastGlobalTurnTokens = globalTurnTokens
return {
action: 'continue',
nudgeMessage: getBudgetContinuationMessage(pct, turnTokens, budget),
continuationCount: tracker.continuationCount,
pct,
turnTokens,
budget,
}
}
if (isDiminishing || tracker.continuationCount > 0) {
return {
action: 'stop',
completionEvent: {
continuationCount: tracker.continuationCount,
pct,
turnTokens,
budget,
diminishingReturns: isDiminishing,
durationMs: Date.now() - tracker.startedAt,
},
}
}
return { action: 'stop', completionEvent: null }
}
The first rule is the 90% line. If the turn is still below that target and the recent token growth does not look weak, the runtime sends a continuation nudge instead of ending the turn.
The second rule is the diminishing-returns check. After at least three
continuations, if both the newest token gain and the previous gain are under
DIMINISHING_THRESHOLD, the system treats that as stalling and stops with a
diminishingReturns completion event instead of nudging again.
Stop hooks are the next stage after the model response ends:
export async function* handleStopHooks(
messagesForQuery: Message[],
assistantMessages: AssistantMessage[],
systemPrompt: SystemPrompt,
userContext: { [k: string]: string },
systemContext: { [k: string]: string },
toolUseContext: ToolUseContext,
querySource: QuerySource,
stopHookActive?: boolean,
)
This function does not merely log. It can trigger prompt suggestion, memory extraction, auto-dream, and hook summaries before the turn fully settles.
Takeaways
- Budget logic protects long-running turns from drifting forever.
- Stop hooks are real post-turn behavior, not just analytics.
- The budget gate and stop hooks together decide how a turn actually ends.