mcp-compact
by mikesmullin
README.md
<p align="center">
<a href="#install">Install</a>
Β·
<a href="#look-then-cut">Look, then cut</a>
Β·
<a href="#the-two-tools">The two tools</a>
Β·
<a href="#safety">Safety</a>
Β·
<a href="#configuration">Configuration</a>
</p>
# ποΈ mcp-compact
**Surgery for an immortal agent session β look at what's eating context, then cut exactly that.**
[Ada](https://github.com/mikesmullin/ada) never restarts. Her [Angela](https://github.com/mikesmullin/angela) session (`*.jsonl`, one event per line) accretes
forever: every user prompt, every reasoning trace, every tool call and result β
plus screenshots and image payloads that each weigh as much as a novella. The
usual options all disappoint: restart the session and she forgets who you are;
summarize aggressively and hard-won facts get paraphrased into mush; truncate
from the front and you amputate the conversation while keeping the bulkiest
junk. You end up with either **no memory or no room to think**.
`mcp-compact` is the opposite. It treats the session file as a patient, not a
log pile: first a read-only report that weighs every event in bytes and tokens
so you and your agent can *choose together* what goes, then a trimmer that
removes exactly what you named β or, when you don't want to choose, a blanket
pass that takes only the provably dead weight. The key result: the agent is
extremely intelligent and strategic about what it removes. It successfully
performs surgery on its own memory β as trivial as deciding what files to
delete from disk when space runs low. Telemetry, system prompts, and tool catalogs are immortal
and never listed or touched. Everything else keeps working: [Ada](https://github.com/mikesmullin/ada) notices a real
run and reloads the trimmed history into memory at the next turn boundary, no
restart dance.
**Why use it**
- **Look before you cut.** `context_analysis` weighs the whole window (bytes,
~tokens, share of budget) and lists one line per event β
`sha6 kind NNNtok: gistβ¦` β heaviest first, pageable to the full session.
- **Surgical or blanket.** Pass `removeShas` from the report to delete exactly
those events, or run empty-handed for the blanket pass: image payloads plus
`tool_call`/`tool_result` pairs. Nothing in between.
- **Pairs never split.** Naming a call takes its result; naming a result takes
its call. The transcript stays coherent.
- **No phantom limbs.** The surgery happens in-session, by the same agent
that keeps talking β so it *knows* what was removed and works around the
gaps instead of hallucinating or tripping over dangling references.
- **The denominator is honest.** The budget is `min([AGL](https://github.com/mikesmullin/agl) per-model table, server
floor)`, so the `% full` number matches the pie ring [Ada](https://github.com/mikesmullin/ada) shows, not a guess.
- **Safe by construction.** `dryRun` defaults true, real runs back up the file
(`*.pre-compact-<stamp>`), refresh the sidecar counts, keep corrupt lines
rather than destroy them, and refuse anything not sent to the provider.
- **Naive on purpose.** No summarization, no semantic merging β what survives is
verbatim. (That's the v1 contract; summarization is the obvious next pass.)
## Install
Built for [Bun](https://bun.sh) + CoffeeScript. Not on npm β run it from local
disk as a stdio MCP server:
```sh
cd /workspace/mcp-compact
bun install
bun ./server.coffee # stdio MCP; see Configuration below
```
Wired into [Ada](https://github.com/mikesmullin/ada) as the `compact` MCP (`prefix: false`), so the model sees
`context_analysis` and `compact_session_history` directly. `compact_session_history`
stays Tom-gated; `context_analysis` runs free.
## Look, then cut
This is the part worth reading. The loop is always the same: weigh the window,
pick the weight, remove it, keep talking.
**1. Weigh the window.** Ask what the session is made of. Totals always cover
the whole window; `limit` + `offset` just page the listing:
```
context_analysis({ sort: "largest", limit: 25 })
```
```
Context window: 2026-09-06
Window: 183420tok / 262144tok (70.0%) in 412 events
Budget 262144tok (gemma-4-26b: AGL 262144, server floor 262144)
Excluded: 96 telemetry/system/reasoning events (never listed or removed)
Showing top 25 of 412 largest (offset=0 limit=25)
a3f9c1 tool_response 18230tok: agent_browser_snapshot => {tabs: [...], β¦}
77bd20 tool_response 15411tok: read_file => {"content": "iVBORw0KGgoβ¦}
...
```
Kinds are the stable vocabulary everywhere (report *and* removal) β only what
the provider actually sees:
`user_prompt`, `tool_call`, `tool_response`, `assistant_response`.
Reasoning traces are UI/jsonl only (AGL never retransmits them), same as
`provider_*` telemetry, `gen_info`, harness events, system prompts, tool
catalogs, and session bookkeeping: excluded from the report and immortal to
the trimmer.
**2. Preview the cut.** Everything is `dryRun: true` unless you say otherwise.
The blanket pass reports what it *would* drop; the targeted pass reports
per-sha resolution (`removed` vs. `no match` / `too short` / `refused`):
```
compact_session_history({ dryRun: true })
compact_session_history({ dryRun: true, removeShas: ["a3f9c1", "77bd20"] })
```
```json
{
"session": "2026-09-06",
"dryRun": true,
"targeted": true,
"eventsBefore": 412,
"eventsAfter": 408,
"removedToolCalls": 2,
"removedToolResults": 2,
"shaDetail": { "a3f9c1": "β¦", "77bd20": "β¦" }
}
```
Sha prefixes need 4+ hex chars; anything shorter is refused, anything matching
only immortal events is refused per sha, and pair-mates ride along automatically.
**3. Make the cut and keep talking.** Drop `dryRun` (or set it false). The
server backs up the jsonl, rewrites it, refreshes the sidecar `.json`
(`eventCount`, `updatedAt`) β and [Ada-back](https://github.com/mikesmullin/ada) notices the real run and reloads the
trimmed history into memory at the next turn boundary. Manual runs while the
back is stopped still apply on next startup. No restart needed either way.
## The two tools
| tool | what it does | writes? |
|---|---|---|
| `context_analysis` | Read-only report: budget/window line, then one `sha6 kind NNNtok: gist` line per event (`largest` or `chrono`, `limit` β€ 1000, `offset` pages). | never |
| `compact_session_history` | Slim the session file. Blanket: drop image-payload events + `tool_call`/`tool_result` pairs. Targeted (`removeShas`): drop exactly those events + pair-mates. | only when `dryRun: false` |
Resolution order for the session file: explicit `sessionId` (basename without
`.jsonl`), else the pointer file (default
`/workspace/ada/.angela/ada-session`). Bad ids and missing files are errors,
not guesses.
What counts as an image: `data:image/β¦;base64` URLs, `image_url` parts, or any
base64 run long enough to only be a picture (tool-result metadata like
`{mimeType, bytes}` is a few dozen bytes and is kept).
## Safety
The invariants, for when you want the full contract:
- **Provider-sent kinds, nothing else.** Only `user_prompt`, `tool_call`,
`tool_response`, `assistant_response` are ever eligible. Reasoning traces,
telemetry, system, catalogs, and bookkeeping are invisible to the report and
immortal to the trimmer.
- **Pairs never split.** A dropped call takes its result(s); a dropped result
takes its call β by `tool_call_id`, in both directions.
- **Pixels, not metadata.** The image detector keys on payload shape, so small
metadata survives while screenshots and pasted pictures go.
- **Corrupt lines survive.** A line that won't parse is kept rather than
destroyed.
- **Every real run is reversible.** Pre-rewrite backup beside the session file;
sidecar counts refreshed so nothing downstream lies about the new size.
- **Dry first.** `dryRun` defaults true on the write tool. Preview counts, then
commit.
## Configuration
Env vars (all optional; shown with defaults):
| var | default | what it does |
|---|---|---|
| `ADA_ROOT` | `/workspace/ada` | anchor for the session-dir and pointer defaults |
| `ADA_SESSION_DIR` | `$ADA_ROOT/.angela/sessions` | where `<id>.jsonl` + sidecar `<id>.json` live |
| `ADA_SESSION_FILE` | `$ADA_ROOT/.angela/ada-session` | pointer file holding the live session id |
| `AGL_CONFIG_PATH` | `~/.config/agl/config.yaml` | parsed directly for the per-model `context_windows` table |
| `ADA_SERVER_CTX` | `262144` | server floor; budget is `min(AGL table, floor)` |
## Contributing
Naive by design is a starting point, not a destination. The next pass is
summarization / semantic merging on top of the sha-targeted machinery β the
report already speaks the vocabulary (`removeShas`) the summarizer will need.
Until then: no new eligible kinds without updating `displayType`,
`compactLines`, and the pair-expansion together, and keep the report's one-line
gist (`sha6 kind NNNtok: β¦`) parseable β [Ada](https://github.com/mikesmullin/ada) reads it, not just humans.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues