attini 0.0.1

CLI coding agent that aims to be as autonomous as it can be, without ever leaving your control
Documentation
# Conversation compaction

**Status:** Implemented and in use. This document records the design and
behavior of attini's conversation compaction: when it fires, what it keeps,
and how it avoids overflowing the context window.

## Overview

A long session eventually exceeds the model's context window. attini handles
this by summarising older conversation records into a compact summary and
keeping only a recent tail, so the next model call fits within the window.

Two independent pieces work together:

1. **Trigger** — decide *when* to compact (`try_auto_compact`).
2. **Cutoff** — decide *what* to keep vs. fold into the summary
   (`compaction_cutoff`), and how the summary is rendered
   (`render_summary_transcript`).

## Trigger: when compaction runs

`try_auto_compact` (`src/tell_cli.rs`) judges the need to compact from two
independent signals via the pure helper
`should_auto_compact(latest, total_chars)`:

```rust
fn should_auto_compact(latest: u64, total_chars: usize) -> bool {
    latest >= COMPACTION_TRIGGER_TOKENS || total_chars > RECORDS_TOTAL_MAX_CHARS
}
```

- `latest` is the last successful turn's `prompt_tokens`
  (`session.latest_prompt_tokens()`), compared against
  `COMPACTION_TRIGGER_TOKENS` (`16_384`).
- `total_chars` is the raw character size of the real records since the last
  summary, compared against `RECORDS_TOTAL_MAX_CHARS` (`250_000`). It is
  computed only when `latest < COMPACTION_TRIGGER_TOKENS` (so a normal-size
  history does not pay the extra scan cost).

### Why two signals (the stale-token hole)

`latest_prompt_tokens` (`src/session.rs`) scans `token_usage` records and
returns the **last successfully recorded** `prompt_tokens` value. That value
is absent or stale if the previous turn did not complete normally — for
example it was suspended awaiting approval right after a very large tool
result was appended. In that case the real records can be far larger than the
last recorded value implies, and the token-only trigger would early-return
without compacting.

The record-size signal closes that hole, at the cost of a lazy extra scan
only when the token signal is not already met.

## Cutoff: what is folded into the summary

`compaction_cutoff` (`src/tell_cli.rs`) chooses the oldest record to keep.
It walks from a safe boundary near the tail toward the start, folding more
into the summary while the **whole retained tail** exceeds the budget:

- `KEEP_RECENT_RECORDS_TARGET` (`10`) — keep at least this many recent
  records as raw history.
- `RETAINED_TAIL_MAX_CHARS` (`250_000`) — if the kept tail exceeds this
  budget, fold more into the summary.
- A **safe boundary** is a record that can be cut at without breaking a
  `assistant`/`tool` pair (the `safe_tail_start` helper avoids splitting a
  `tool_call` from its matching `tool` result).

The function no longer early-returns `None` when `records.len() <= target_keep`:
for a short history it starts at `safe_tail_start` (which returns `0`) and
walks forward while the whole history exceeds the budget, so a few records
dominated by one huge tool result are folded into a summary rather than
kept raw.

`None` is returned only when the retained tail stays within budget.

## Summary rendering

The summariser sends a **bounded prose transcript** rather than raw
`ChatMessage` records. `render_summary_transcript` collapses each record to a
short line, capping tool-call args (`90` chars), tool results (`200` chars),
and assistant/user text (`16k` chars each); the newest side is kept within
`SUMMARY_MAX_CHARS` (`200_000`), with a note when old parts are dropped.
This avoids feeding a huge `reasoning_content` or tool-result JSON back to
the model during compaction.

## Guards

- `try_auto_compact` returns early when `pending.json` exists, so it fires
  only on a fresh `Continuation::Prompt` (never on `--approve`).
- It fires on the first turn of a resumed session only; after compaction the
  summary replaces the huge history so a second pass sees a small total.
- Compaction is automatic only; there is no manual `compact` subcommand. The
  log is bounded by automatic pruning, described in [pruning.md]pruning.md.

## Related

- [pruning.md]pruning.md — automatic physical pruning of the log.
- `docs/bug/conversation-log-bloat.md` — the investigation that motivated the
  bounded prose summariser and the retained-tail cap.