Studio Notebook

Claude Code Atlas

Query Budget And Stop Hooks

Learn how the turn decides whether to continue and what happens after a model response stops.

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.