---
title: "Query Engine — Claude Code Internals — Vlad Arbatov"
description: "Conversation loop, streaming, token management, error recovery, compaction"
image: "https://arbatov.dev/share-claude.png"
---

# Query Engine & Conversation System

## Executive Summary

The Claude Code query engine orchestrates multi-turn conversations with the Claude API through two primary systems:

1. **QueryEngine** (QueryEngine.ts:1295 lines) - High-level conversation lifecycle management
2. **query()** (query.ts:1729 lines) - Core query loop implementing a sophisticated state machine for API interaction, tool execution, and error recovery

The system handles streaming responses, tool use with permission controls, automatic context compaction, error recovery with fallbacks, cost tracking per-model, and complex multi-turn conversation continuity.

---

## Architecture Overview

```
QueryEngine.submitMessage()
    ↓ (async generator)
processUserInput() → message array
    ↓
query() → queryLoop() [state machine]
    ↓
deps.callModel() → queryModelWithStreaming()
    ↓
Anthropic API (streaming or non-streaming)
```

---

## QueryEngine.ts - Conversation Wrapper

**File Size:** 1295 lines
**Purpose:** Manages conversation lifecycle, session persistence, and SDK message formatting

### Core Class: QueryEngine (Lines 184-1177)

```typescript
export class QueryEngine {
  private config: QueryEngineConfig
  private mutableMessages: Message[]        // Persists across turns
  private abortController: AbortController
  private permissionDenials: SDKPermissionDenial[]
  private totalUsage: NonNullableUsage
  private discoveredSkillNames = new Set<string>()
  private loadedNestedMemoryPaths = new Set<string>()
}
```

#### Constructor (Lines 200-207)
- Initializes from QueryEngineConfig
- Sets mutableMessages from initialMessages or empty array
- Creates abort controller and empty usage tracking

#### submitMessage() Generator (Lines 209-1156)

**Signature:** `async *submitMessage(prompt, options?): AsyncGenerator<SDKMessage>`

**Major Phases:**

1. **System Prompt Assembly (Lines 284-326)**
   - Fetches default system prompt via `fetchSystemPromptParts()`
   - Optionally injects memory-mechanics prompt (if CLAUDE_COWORK_MEMORY_PATH_OVERRIDE set)
   - Layers custom, default, memory, and append prompts
   - Returns single assembled SystemPrompt

2. **User Input Processing (Lines 335-428)**
   - Creates ProcessUserInputContext for state management
   - Calls `processUserInput()` to handle slash commands and attachments
   - Pushes new messages to mutableMessages
   - Records user message to transcript (awaited unless bare mode)

3. **Local Command Return (Lines 556-639)**
   - If shouldQuery=false, returns local command output immediately
   - Yields synthetic assistant messages for command output
   - Returns success result without calling API

4. **Query Loop (Lines 675-1049)**
   - Yields system init message with tools, MCP clients, model info
   - Calls `query()` generator with full message context
   - Consumes streaming events, messages, and tool summaries
   - Accumulates totalUsage across all messages

5. **Message Recording (Lines 716-854)**
   - Records assistant/user/compact_boundary to transcript
   - Fire-and-forget for assistant messages (transaction-safe)
   - Awaited for other types to maintain order
   - Acknowledges user messages after first write (if replayUserMessages)

6. **Event Handling (Lines 788-826)**
   - message_start: resets currentMessageUsage
   - message_delta: updates usage, captures stop_reason
   - message_stop: accumulates to totalUsage
   - Yields partial messages if includePartialMessages=true

7. **Completion Checks (Lines 971-1048)**
   - Validates budget not exceeded (maxBudgetUsd)
   - Counts structured output retries against limit
   - Calls `isResultSuccessful()` on final message

8. **Result Extraction (Lines 1120-1155)**
   - Finds last assistant or user message
   - Extracts text from final text block
   - Yields result with usage, cost, model breakdown, permission denials

### QueryEngineConfig Type (Lines 130-173)

Essential configuration parameters:

```typescript
type QueryEngineConfig = {
  cwd: string
  tools: Tools
  commands: Command[]
  mcpClients: MCPServerConnection[]
  agents: AgentDefinition[]
  canUseTool: CanUseToolFn                    // Permission gating
  getAppState: () => AppState
  setAppState: (f: (prev: AppState) => AppState) => void
  initialMessages?: Message[]
  readFileCache: FileStateCache              // For file state tracking
  customSystemPrompt?: string
  appendSystemPrompt?: string
  userSpecifiedModel?: string
  fallbackModel?: string                     // 3P capacity fallback
  thinkingConfig?: ThinkingConfig
  maxTurns?: number
  maxBudgetUsd?: number
  taskBudget?: { total: number }            // API task_budget (beta)
  jsonSchema?: Record<string, unknown>
  verbose?: boolean
  replayUserMessages?: boolean               // SDK message acknowledgement
  handleElicitation?: ToolUseContext['handleElicitation']
  includePartialMessages?: boolean
  setSDKStatus?: (status: SDKStatus) => void
  abortController?: AbortController
  orphanedPermission?: OrphanedPermission
  snipReplay?: (yieldedSystemMsg, store) => snipResult | undefined
}
```

---

## query.ts - Query Loop State Machine

**File Size:** 1729 lines
**Purpose:** Core loop managing API calls, tool execution, compaction, and error recovery

### Entry Point: query() (Lines 219-239)

Wrapper that delegates to queryLoop() and handles command lifecycle:

```typescript
export async function* query(params: QueryParams)
```

Returns: `AsyncGenerator<StreamEvent | Message | ...>`

### QueryParams Type (Lines 181-199)

```typescript
type QueryParams = {
  messages: Message[]
  systemPrompt: SystemPrompt
  userContext: { [k: string]: string }
  systemContext: { [k: string]: string }
  canUseTool: CanUseToolFn
  toolUseContext: ToolUseContext
  fallbackModel?: string
  querySource: QuerySource
  maxOutputTokensOverride?: number
  maxTurns?: number
  skipCacheWrite?: boolean
  taskBudget?: { total: number }
  deps?: QueryDeps
}
```

### queryLoop() Main State Machine (Lines 241-1729)

**State Type** (Lines 204-217):

```typescript
type State = {
  messages: Message[]
  toolUseContext: ToolUseContext
  autoCompactTracking: AutoCompactTrackingState | undefined
  maxOutputTokensRecoveryCount: number
  hasAttemptedReactiveCompact: boolean
  maxOutputTokensOverride: number | undefined
  pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
  stopHookActive: boolean | undefined
  turnCount: number
  transition: Continue | undefined  // Reason for continuation
}
```

**Main Loop:** `while (true)` with state mutations for continuation (Lines 307-1728)

#### Phase 1: Initialization (Lines 265-305)

- Snapshot QueryConfig once per entry
- Initialize BudgetTracker if TOKEN_BUDGET feature enabled
- Setup taskBudgetRemaining for compaction carryover
- Start memory prefetch (non-blocking, settled during turns)
- Setup skill discovery prefetch

#### Phase 2: Message Preparation (Lines 365-543)

**2.1 - Snip Compaction** (Lines 401-410)
- Calls `snipModule.snipCompactIfNeeded(messagesForQuery)`
- Yields boundary message if snip executed
- Returns snipTokensFreed for autocompact threshold adjustment

**2.2 - Microcompaction** (Lines 413-426)
- Calls `deps.microcompact()` for prompt cache editing
- Captures pendingCacheEdits for deferred boundary message after streaming

**2.3 - Context Collapse** (Lines 440-447)
- Calls `contextCollapse.applyCollapsesIfNeeded()`
- Non-yielding: projects view over collapse store, not actual messages
- Runs before autocompact so collapse can reduce threshold

**2.4 - Autocompaction** (Lines 453-543)
- Calls `deps.autocompact()` with full context state
- If successful:
  - Logs tengu_auto_compact_succeeded event with token deltas
  - Captures taskBudgetRemaining from pre-compact final window
  - Yields compact_boundary message
  - Resets tracking with new turnId, turnCounter=0
- If failed:
  - Propagates consecutiveFailures count to circuit breaker

#### Phase 3: Blocking Limit Check (Lines 628-648)

Prevents tokens from exceeding hard limit (unless compaction just ran):

```typescript
if (!compactionResult && !collapseOwnsIt && !reactiveCompactEnabled) {
  const { isAtBlockingLimit } = calculateTokenWarningState(
    tokenCountWithEstimation(messagesForQuery) - snipTokensFreed,
    mainLoopModel,
  )

  if (isAtBlockingLimit) {
    yield createAssistantAPIErrorMessage(PROMPT_TOO_LONG_ERROR_MESSAGE)
    return { reason: 'blocking_limit' }
  }
}
```

#### Phase 4: API Call & Streaming (Lines 652-954)

**4.1 - Retry Loop** (Lines 654-954)

```typescript
while (attemptWithFallback) {
  attemptWithFallback = false

  for await (const message of deps.callModel({...})) {
    // Streaming consumption
  }
}
```

**Streaming Fallback Detection** (Lines 709-741):
- If fallback occurred: tombstone orphaned assistant messages
- Clear tool_results, reset StreamingToolExecutor
- Continue retry with new model

**Message Processing** (Lines 747-787):
- Backfill observable tool inputs before yielding
- Clone only if new fields added (preserve prompt cache byte-exactness)

**Error Withholding** (Lines 799-825):
- Withhold prompt-too-long errors (may recover via collapse/reactive compact)
- Withhold max-output-tokens errors (may escalate or multi-turn recover)
- Withhold media-size errors (reactive compact can strip)
- Still push to assistantMessages array for recovery checks

**Streaming Tool Execution** (Lines 838-862):
- If StreamingToolExecutor active:
  - Add tool blocks as content_block_start arrives
  - Yield completed tool results as they finish
  - Prevents tool execution blocking on streaming
- Accumulate all results in toolResults array

**Fallback Handling** (Lines 893-954):
- Catch FallbackTriggeredError
- Switch currentModel to fallbackModel
- Yield missing tool_result blocks for orphaned tool_uses
- Clear state, strip thinking signatures
- Log tengu_model_fallback_triggered event
- Yield warning system message about switch
- Retry entire request with new model

**Error Handling** (Lines 955-997):
- Log query error with telemetry (message count, tool count)
- Catch ImageSizeError/ImageResizeError → user-friendly message
- Yield missing tool_result blocks for incomplete streams
- Return { reason: 'model_error', error }

#### Phase 5: Post-Streaming & Abort (Lines 999-1052)

- Execute post-sampling hooks async (non-blocking)
- Check abort signal (user Ctrl+C)
- Yield missing tool_result blocks if aborted
- Cleanup computer use sessions on abort
- Return { reason: 'aborted_streaming' }

#### Phase 6: Recovery Paths (Lines 1062-1183)

**Prompt-Too-Long (PTL) Recovery:**

1. **Context Collapse Drain** (Lines 1089-1117)
   - Calls `contextCollapse.recoverFromOverflow()`
   - If committed > 0: state continues loop (cheap, granular context preserved)

2. **Reactive Compact** (Lines 1119-1166)
   - Calls `reactiveCompact.tryReactiveCompact()`
   - One shot per turn (hasAttemptedReactiveCompact guards)
   - Captures taskBudgetRemaining before compaction
   - Full summary, loses granularity

3. **Surface Error** (Lines 1176-1182)
   - If recovery fails: yield withheld message
   - Call executeStopFailureHooks
   - Return early (skip stop hooks — model never produced valid response)

**Max Output Tokens (MOT) Recovery:**

1. **Escalation** (Lines 1195-1220)
   - If capped 8k default and MOT error: retry at ESCALATED_MAX_TOKENS (64k)
   - No meta message, single escalation per turn
   - Guarded by maxOutputTokensOverride check

2. **Multi-Turn Recovery** (Lines 1223-1251)
   - Send recovery message: "Output token limit hit. Resume directly..."
   - Increment maxOutputTokensRecoveryCount
   - Guard: MAX_OUTPUT_TOKENS_RECOVERY_LIMIT (3 attempts)

#### Phase 7: Stop Hooks (Lines 1267-1306)

- Call `handleStopHooks()` generator (query/stopHooks.ts)
- If preventContinuation: return { reason: 'stop_hook_prevented' }
- If blockingErrors: continue loop with errors injected (triggers retry)

#### Phase 8: Token Budget (Lines 1308-1355)

If TOKEN_BUDGET feature enabled:

```typescript
const decision = checkTokenBudget(budgetTracker, agentId, budget, turnTokens)

if (decision.action === 'continue') {
  // Send nudge message, increment continuation count
  state = {..., transition: { reason: 'token_budget_continuation' }}
  continue
} else if (decision.completionEvent?.diminishingReturns) {
  logEvent('tengu_token_budget_completed', {..., diminishingReturns: true})
  return { reason: 'completed' }
}
```

#### Phase 9: Tool Execution (Lines 1360-1409)

**Streaming vs. Classic:**

```typescript
const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()  // Non-blocking
  : runTools(toolUseBlocks, ...)                 // Blocking, serial
```

**Result Processing:**
- Yield messages from tool execution
- Track hook_stopped_continuation attachments
- Normalize and push to toolResults array

#### Phase 10: Tool Use Summary (Lines 1411-1482)

Haiku-based summary of tool calls (non-blocking):

```typescript
nextPendingToolUseSummary = generateToolUseSummary({
  tools: toolInfoForSummary,
  signal: abortController.signal,
  lastAssistantText
})
  .then(summary => {
    if (summary) {
      return createToolUseSummaryMessage(summary, toolUseIds)
    }
    return null
  })
  .catch(() => null)  // Non-critical
```

Consumed between turns (Lines 1055-1060):

```typescript
if (pendingToolUseSummary) {
  const summary = await pendingToolUseSummary
  if (summary) {
    yield summary
  }
}
```

#### Phase 11: Attachment Collection (Lines 1547-1628)

**11.1 - Queued Commands** (Lines 1570-1578):
- Drain task-notification and prompt commands
- Skip slash commands (processed after turn ends)
- Filter by agentId for subagents

**11.2 - Memory Attachments** (Lines 1599-1614):
- Consume memory prefetch if settled
- Filter duplicates vs. readFileState (cumulative)
- Emit after tool results

**11.3 - Skill Discovery** (Lines 1620-1628):
- Prefetched during model streaming
- Collected and yielded now

#### Phase 12: Loop Continuation (Lines 1705-1728)

**Max Turns Check:**

```typescript
if (maxTurns && nextTurnCount > maxTurns) {
  yield createAttachmentMessage({
    type: 'max_turns_reached',
    maxTurns,
    turnCount: nextTurnCount,
  })
  return { reason: 'max_turns', turnCount: nextTurnCount }
}
```

**State Update for Next Iteration:**

```typescript
const next: State = {
  messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
  toolUseContext: toolUseContextWithQueryTracking,
  autoCompactTracking: tracking,
  turnCount: nextTurnCount,
  maxOutputTokensRecoveryCount: 0,  // Reset per turn
  hasAttemptedReactiveCompact: false,
  pendingToolUseSummary: nextPendingToolUseSummary,
  transition: { reason: 'next_turn' },
}
state = next
// while (true) continues...
```

---

## Message Formats

### Core Message Union Type

Messages flow through query loop as discriminated union (types/message.ts):

| Type | Content | Key Fields |
|------|---------|-----------|
| assistant | Text, thinking, tool_use blocks | `message.usage`, `stop_reason`, `apiError` |
| user | Text or content blocks | `toolUseResult`, `sourceToolAssistantUUID` |
| system | Notification text | `subtype`, `error`, `compactMetadata` |
| attachment | Structured data (memory, file, hook result) | `attachment.type` |
| progress | Tool execution progress | `data`, `toolUseID` |
| stream_event | Raw API events | `event` (message_start, content_block_delta, ...) |
| tool_use_summary | Haiku summary of tools | `summary`, `preceding_tool_use_ids` |
| compact_boundary | Compaction marker | `compact_metadata` |
| tombstone | Message removal signal | `message` (to delete) |

### Message API Normalization

Function `normalizeMessagesForAPI()` (utils/messages.ts):

- Strips signature blocks (thinking blocks with signatures)
- Normalizes content arrays to API format
- Ensures tool_result blocks have matching tool_use
- Excludes compact_boundary messages

---

## Streaming and Content Blocks

### Stream Event Handling (services/api/claude.ts:1979-2297)

Raw `Stream<BetaRawMessageStreamEvent>` consumed line-by-line:

#### message_start (Lines 1980-1994)
- Sets partialMessage (template for final message)
- Captures ttftMs (time to first token)
- Updates usage from message.usage
- Stores research metadata (ant-only)

#### content_block_start (Lines 1995-2051)
- Initializes contentBlocks[index]:
  - tool_use: input = ''
  - text: text = ''
  - thinking: thinking = '', signature = ''
  - server_tool_use: input = {}
  - connector_text: connector_text = '', signature = ''

#### content_block_delta (Lines 2053-2169)
- input_json_delta: accumulates into contentBlock.input
- text_delta: accumulates into contentBlock.text
- thinking_delta: accumulates into contentBlock.thinking
- signature_delta: captures block.signature
- connector_text_delta: accumulates connector_text (feature gated)

#### content_block_stop (Lines 2171-2211)
- Normalizes block via `normalizeContentFromAPI()`
- Creates AssistantMessage with completed block
- Yields message immediately
- Pushes to newMessages array

#### message_delta (Lines 2213-2293)
- Updates usage with final token counts
- Updates stop_reason (only arrives here, not content_block_stop)
- **Direct mutation:** Updates lastMsg.message.usage and stop_reason
  - Critical: Preserves transcript write queue reference
  - Write queue serializes lazily (100ms flush), holds reference to message.message
- Calls `addToTotalSessionCost()` to track cost
- Handles refusal, max_tokens, and context_window_exceeded errors

#### message_stop (Line 2295-2296)
- No-op (stream ends after this event)

### Streaming Idle Watchdog (Lines 1874-1929)

Timeout on hung connections:

```typescript
STREAM_IDLE_TIMEOUT_MS = 90_000  // Configurable via env
streamIdleTimer = setTimeout(() => {
  streamIdleAborted = true
  logEvent('tengu_streaming_idle_timeout', {...})
  releaseStreamResources()
}, STREAM_IDLE_TIMEOUT_MS)
```

---

## Token Management and Cost Tracking

### Token Counting

**tokenCountWithEstimation()** (utils/tokens.ts):
- Uses actual input_tokens from last API response as anchor
- Estimates remainder from message history
- Adjusts for cache_read_input_tokens (don't count against estimate)

### BudgetTracker (query/tokenBudget.ts:6-20)

```typescript
type BudgetTracker = {
  continuationCount: number    // How many times continued
  lastDeltaTokens: number      // Tokens since last check
  lastGlobalTurnTokens: number // Global turn token watermark
  startedAt: number            // Timestamp for duration
}
```

### checkTokenBudget() (query/tokenBudget.ts:45-93)

Decision logic:

```typescript
const pct = (turnTokens / budget) * 100
const isDiminishing =
  continuationCount >= 3 &&
  deltaSinceLastCheck < 500 &&
  lastDeltaTokens < 500

if (!isDiminishing && turnTokens < budget * 0.9) {
  return { action: 'continue', ... }
}

if (isDiminishing || continuationCount > 0) {
  return { action: 'stop', completionEvent: {...} }
}
```

### Cost Tracking (cost-tracker.ts)

**addToTotalSessionCost()** (Lines 278-323):

```typescript
export function addToTotalSessionCost(
  cost: number,
  usage: Usage,
  model: string,
): number
```

- Adds to per-model usage tracking
- Records to metrics counter
- Recursively handles advisor usage
- Returns total cost (model + advisor)

**Model Usage Aggregation:**

Groups by canonical model name:

```typescript
{
  [modelName]: {
    inputTokens: number
    outputTokens: number
    cacheReadInputTokens: number
    cacheCreationInputTokens: number
    webSearchRequests: number
    costUSD: number
    contextWindow: number
    maxOutputTokens: number
  }
}
```

**Session Persistence:**

- `saveCurrentSessionCosts()` → persists to project config
- `restoreCostStateForSession()` → retrieves on resume (if session ID matches)
- `getStoredSessionCosts()` → retrieves without restore

---

## Error Handling and Recovery

### Error Categories (services/api/errors.ts, withRetry.ts)

**Retryable (with exponential backoff):**
- Network timeouts (APIConnectionTimeoutError)
- 429 (rate limit)
- 5xx server errors (except 503 without Retry-After)

**Non-Retryable (thrown immediately):**
- 400 (invalid request)
- 401 (authentication)
- 403 (forbidden)
- 404 (not found)

**Capacity/Fallback (triggers model switch):**
- 529 (capacity) → FallbackTriggeredError

**Special Handling:**
- Prompt too long (413) → recovered via context collapse/reactive compact
- Max output tokens → escalated (8k→64k) or multi-turn
- Media size error → reactive compact strips images

### withRetry() Wrapper (services/api/withRetry.ts)

```typescript
await withRetry(
  () => anthropic.beta.messages.create({...}),
  asyncFn,
  { maxRetries, model, thinkingConfig }
)
```

- Exponential backoff with jitter
- Logs retry count and error
- Throws CannotRetryError for non-retryable
- Throws FallbackTriggeredError for 529

### Recovery Flow in queryLoop()

| Error | Recovery Strategy | Line Range | Outcome |
|-------|-------------------|-----------|---------|
| 413 Prompt too long | Collapse drain → reactive compact | 1085-1183 | Continue turn or surface error |
| Max output tokens (escalate) | Retry at 64k instead of 8k | 1195-1220 | Continue (single escalation) |
| Max output tokens (multi-turn) | Send recovery message | 1223-1251 | Continue with nudge (max 3 times) |
| Media size error | Reactive compact (strips images) | 1082-1084 | Continue or surface |
| Model fallback (529) | Switch model, strip thinking, retry | 894-954 | Continue (full request retry) |
| Streaming idle timeout | Abort, fail turn | 1874-1928 | No retry |
| Stop hook error | Log, continue (non-critical) | 456-472 | Continue |

---

## Stop Hooks (query/stopHooks.ts)

### Hook Execution Flow (Lines 65-473)

1. **Async Background Services** (Lines 104-157)
   - Job classification (TEMPLATES feature)
   - Prompt suggestion
   - Memory extraction (EXTRACT_MEMORIES feature)
   - Auto-dream

2. **Blocking Hooks** (Lines 175-453)
   - Execute Stop hooks (user-defined scripts)
   - Execute TaskCompleted hooks (if teammate)
   - Execute TeammateIdle hooks
   - Collect errors and continuation flags

3. **Error Handling** (Lines 456-472)
   - Log hook errors (non-critical)
   - Yield warning system message
   - Continue (don't fail turn)

### Hook Result Type (Lines 60-63)

```typescript
type StopHookResult = {
  blockingErrors: Message[]
  preventContinuation: boolean
}
```

If blockingErrors returned → Loop continues with errors injected
If preventContinuation → Loop returns immediately

---

## System Prompt Assembly

### Prompt Parts (utils/queryContext.ts)

Fetched once per submitMessage():

```typescript
const {
  defaultSystemPrompt: SystemPrompt[],
  userContext: { [k: string]: string },
  systemContext: { [k: string]: string }
} = await fetchSystemPromptParts({
  tools,
  mainLoopModel,
  additionalWorkingDirectories,
  mcpClients,
  customSystemPrompt
})
```

### Prompt Layers (QueryEngine.ts:321-325)

Assembled in order:

1. Custom system prompt (if provided)
2. Default system prompt (tools, MCP clients)
3. Memory-mechanics prompt (if memory path override + custom prompt)
4. Append system prompt (caller policies)

Result: Single SystemPrompt via `asSystemPrompt()`

### Context Injection (utils/api.ts)

- `prependUserContext()`: Inserts context dict as first user message block
- `appendSystemContext()`: Appends context dict to system prompt text

---

## Query Configuration and Dependencies

### QueryConfig (query/config.ts)

Immutable snapshot per query() entry:

```typescript
type QueryConfig = {
  sessionId: SessionId
  gates: {
    streamingToolExecution: boolean
    emitToolUseSummaries: boolean
    isAnt: boolean
    fastModeEnabled: boolean
  }
}
```

### QueryDeps (query/deps.ts:21-40)

Testable I/O dependencies:

```typescript
type QueryDeps = {
  callModel: typeof queryModelWithStreaming
  microcompact: typeof microcompactMessages
  autocompact: typeof autoCompactIfNeeded
  uuid: () => string
}

// Production: productionDeps()
// Tests: inject mocks for unit testing
```

---

## Key Invariants

1. **Message Order:** Alternating user/assistant (may skip user if tool loop)
2. **Tool Pairing:** Every tool_use must have matching tool_result
3. **Stop Reason:** Captured from message_delta, null until final message
4. **Cost Accumulation:** Per-model, persisted to config on session switch
5. **Transcript Order:** Assistant fire-and-forget (safe), others awaited
6. **Abort Signals:** Checked before tool execution, streaming, and hooks
7. **Compaction Boundaries:** Arrays spliced to release pre-compact messages
8. **Message References:** Direct mutation of usage/stop_reason preserves write queue refs

---

## Performance Optimizations

1. **Lazy Module Loading:** React/ink loaded only when filtering
2. **Memory Prefetch:** Non-blocking, consumed after tool execution
3. **Skill Discovery Prefetch:** Parallel to model streaming
4. **Tool Use Summary:** Haiku call async, awaited between turns
5. **Streaming Tool Execution:** Non-blocking vs. serial runTools
6. **Transcript Batching:** 100ms flush interval for mutations
7. **Stream Idle Watchdog:** Timeout on hung connections (90s default)
8. **Token Budget:** Monitors, nudges, stops on diminishing returns

---

## Summary

The query engine is a reducer transforming `(state, event) → state` across potentially hundreds of turns, maintaining invariants on message ordering, tool pairing, and cost tracking while providing multiple recovery paths for context/token/capacity failures.

Key design insight: The system uses a **layered compaction strategy** — snip → microcompact → context collapse → autocompact → reactive compact — each progressively more aggressive, trading granularity for space. This allows long conversations to continue far beyond what the raw context window would support.
