# Context Management

Anteroom tracks token usage across the conversation and automatically manages context window limits.

## Token Tracking

Token counting uses **tiktoken** (`cl100k_base` encoding) with a character-estimate fallback (1 token per 4 chars) when tiktoken is unavailable.

| Threshold | What happens |
|---|---|
| **80,000 tokens** | Yellow warning: "Use `/compact` to free space" |
| **100,000 tokens** | Auto-compact triggers automatically before the next prompt |
| **128,000 tokens** | Effective context window ceiling |

## Context Footer

After each response, the context footer shows:

```
  [====----------------] 12,340/128,000 tokens (10%) | response: 482 | 3.2s | 87,660 until auto-compact
```

- **Progress bar** with color coding: green (<50%), yellow (50--75%), red (>75%)
- Current token count / max context window
- Response token count for the current turn
- Total elapsed thinking time
- Remaining tokens until auto-compact fires

## Compact

Use `/compact` to manually summarize and compact the conversation history. Auto-compact triggers automatically at 100K tokens.

### Boundary-based compaction

By default, compaction preserves a **tail** of recent messages intact and only summarizes the older portion of the conversation. This keeps recent tool-use context, file edits, and conversational flow while still reducing token pressure.

The compacted shape looks like:

```
[summary message]      <- AI-generated summary of older messages
[user message]         <- first preserved tail message (always role: user)
[assistant message]
[tool results]
...                    <- remaining recent messages preserved intact
```

Three rules guarantee a safe split point:

1. **No orphaned tool results** — the split never lands mid-tool-sequence.
2. **Provider-safe ordering** — the preserved tail always starts with a `role: "user"` message, satisfying Anthropic and Bedrock validation.
3. **Fallback** — if no valid split exists (e.g. too few messages), the entire conversation is summarized as before.

Configure how many recent messages to preserve:

```yaml
compaction:
  preserve_tail: 6    # default; set to 0 for full-summary behavior
```

Or via environment variable: `AI_CHAT_COMPACT_PRESERVE_TAIL=6`.

The legacy `cli.compact_preserve_tail` key is still accepted as a deprecated alias.

### What the summary preserves

The AI generates a concise summary preserving:

- Key decisions and conclusions
- File paths that were read, written, or edited
- Important code changes and their purpose
- Current task state
- Errors encountered and how they were resolved

Tool call outputs are truncated to 500 characters each in the summary prompt. After compacting:

```
  Compacted 47 messages -> 7 messages
  ~32,140 -> ~4,890 tokens
```

!!! info
    Minimum 4 messages required to compact. This prevents compacting a nearly-empty conversation.

### Session state rehydration

In addition to the LLM-generated summary, the compaction service appends a
**bounded, deterministic** `<session_state>` block to the summary message
content. It captures structured context extracted directly from tool call
arguments and results — never LLM-generated — so the next turn retains a
reliable pointer to recent working state:

```
<session_state>
Files read: src/foo.py, src/bar.py, tests/test_foo.py
Files written: src/baz.py
Files edited: src/foo.py
Working dir: /path/to/repo
Last errors: bash: ModuleNotFoundError: No module named 'xyz'
</session_state>
```

Only non-empty subsections are included. If the summarised region has no
tool calls at all, no block is appended.

**What goes in:**

- File paths touched by `read_file`, `write_file`, `edit_file` (deduplicated,
  capped, most recent kept per category)
- Last working directory hinted by `bash` `cd <path>` segments or an explicit
  `cwd` kwarg
- Unresolved tool errors — an error is considered resolved if a later call
  with the same retry identity succeeds. File tools key on path; bash keys
  on the leading command token (e.g. `pytest`, `python:pytest` for
  `python -m pytest`) so a later unrelated bash success (`ls`) does not
  silently resolve an earlier failed `pytest`

**What does NOT go in:** plan state, RAG sources, system prompt, space
instructions, or skill catalog — these survive compaction already because
they live outside the message list.

Configure via:

```yaml
compaction:
  compact_rehydrate: true              # default; set false to disable
  compact_rehydrate_max_files: 20      # per-category cap (read/written/edited)
  compact_rehydrate_max_errors: 5      # max unresolved errors preserved
```

Or via environment variables: `AI_CHAT_COMPACT_REHYDRATE`,
`AI_CHAT_COMPACT_REHYDRATE_MAX_FILES`, `AI_CHAT_COMPACT_REHYDRATE_MAX_ERRORS`.

On re-compaction, any prior `<session_state>` block is stripped from the
compaction input before summarisation and from the LLM output before the
fresh block is appended — so the block is never nested or duplicated.

## Staged reactive overflow recovery

When a request to the model actually exceeds the context window and the provider
returns a `context_length_exceeded` error, the agent loop recovers through four
staged strategies in order. Cheaper strategies fire first so the conversation is
preserved as much as possible — full LLM compaction is only used as a last resort.

| # | Strategy | Cost | Effect |
|---|----------|------|--------|
| 1 | **Truncate oversized tool outputs** | Free (byte slicing) | Trims any individual `role: tool` message whose content exceeds `tool_output_max_chars`, appending a retry hint |
| 2 | **Collapse historical tool results** | Free (structured truncation) | Compacts *all* tool-result messages in older turn groups to ~200 chars, preserving structured fields (exit_code, error, path) |
| 3 | **Drop older turn groups** | Free (deterministic summary) | Drops older turn groups and replaces them with a compaction summary message matching `/compact` output shape. Preserves the most recent 4 turn groups |
| 4 | **Full LLM compaction** | One LLM call | Summarises the whole older portion via the model. Same behaviour as `/compact` |

Each strategy reports whether it made progress. The first one that reports progress
wins and the loop retries the request. If none make progress, the loop surfaces a
recovery-failed error.

All intermediate strategies are **turn-group aware** — they never split an
`assistant` message with `tool_calls` from its subsequent `role: "tool"` results.
Strategy 3 delegates boundary selection to the same walk-back rules used by
`compact_messages()` so the preserved tail is always provider-safe (starts with
`role: "user"`).

Each strategy emits a short notification token stream visible in the UI:

- `*Context limit reached — tool output was too large. Truncated and retrying with smaller scope...*`
- `*Context limit reached — collapsed historical tool results, retrying...*`
- `*Context limit reached — dropped older conversation turns, retrying...*`
- `*Context limit reached — compacting conversation and retrying...*`
