# 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.

## Proactive microcompact

Before each LLM call, the agent loop runs a cheap, deterministic,
**in-memory** history sweep that trims oversized content before it can
trigger a context overflow. Microcompact is complementary to the
per-tool-ingestion compaction (`services/tool_result_compact.py`) and the
reactive overflow ladder below — it runs *proactively* every turn.

Phase 1 ships one strategy:

| Strategy | Effect |
|---|---|
| **`oversized_tool_outputs`** | Sweeps every `role: "tool"` message whose content exceeds `cli.tool_output_max_chars` and replaces it with the truncated head plus a retry hint. Same strategy implementation as reactive Strategy 1 (single source of truth). |

Microcompact is **in-memory only** — it never writes to SQLite. The
trade-off is that trims are not durable across session resume; a resumed
conversation starts fresh, and microcompact will sweep again on the next
turn if any oversized tool outputs remain in the loaded history.

When at least one message is trimmed, the agent loop emits a short
notification token: `*Trimmed oversized tool outputs to stay within
context limits...*`. When nothing needs trimming, no notification fires.

Disable the stage entirely with:

```yaml
compaction:
  microcompact_enabled: false
```

Or via environment variable: `AI_CHAT_MICROCOMPACT_ENABLED=false`.

## 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...*`

The retry cap (default 4) is configurable via
`compaction.reactive_max_attempts` (env: `AI_CHAT_REACTIVE_MAX_ATTEMPTS`).
Strategy 4's LLM summary is cancel-aware — pressing Escape (CLI) or
disconnecting (web) aborts the provider call cleanly instead of hanging.
