ADR 0029 — Summary Bricks for Escalation Context¶
Status¶
Accepted
Context¶
When milk escalates to Claude Code, it injects the local session history via
--append-system-prompt-file (ADR 0004). The previous implementation serialised
the full sess.History into a flat transcript. For long sessions this exceeded
165K tokens — leaving fewer than 35K tokens for actual work. Claude Code's
autocompact would trigger, refill within 2–3 turns, and trip the rapid_refill_breaker
circuit breaker, aborting the session entirely.
Two additional problems compounded this:
- On --resume, the same full history was re-injected on top of Claude's own
accumulated conversation, doubling the context load.
- Tool-result verbosity was unbounded (a per-result 500-char cap existed but
there was no cap on the number of turns).
Decision¶
Replace the raw-history injection with summary bricks: four maintained fields
on Session, rebuilt eagerly after each turn.
New Session fields¶
| Field | Description |
|---|---|
current_need |
One-sentence description of what the user is trying to accomplish. Updated by <milk:need:NONCE> tag from either agent on context switch. |
escalation_brief |
Tactical reason set by escalate_to_claude(reason). Overwritten on each agent-triggered escalation. |
last_local_summary |
Pre-rendered, sanitized, budget-capped summary of local-agent turns since the last Claude turn. |
last_claude_summary |
Pre-rendered, sanitized, budget-capped summary of Claude turns (reserved for future demotion). |
Context injected per situation¶
| Situation | Injected |
|---|---|
First escalation, user /escalate |
identity + current_need + last_local_summary + need/percept instructions + percepts |
First escalation, agent escalate_to_claude() |
identity + escalation_brief + current_need + last_local_summary + need/percept instructions + percepts |
--resume (any trigger) |
identity + current_need + last_local_summary + need/percept instructions + percepts |
On --resume, Claude already has its own conversation history — only the local
activity since the last Claude session plus the current goal are injected.
Sanitization in brick rebuild¶
- Consecutive identical tool calls (same name, across consecutive turns) are
collapsed: first call shown +
[N more X calls collapsed]. - Tool results are truncated at 500 characters.
- Turns are included newest-first; the oldest are dropped when the character budget is exhausted.
- An empty Claude session (0 tool calls AND < 200 chars of assistant text) does
not update
last_claude_summary.
<milk:need:NONCE> tag¶
Both agents are instructed (via NeedInstruction in the system prompt) to emit:
<milk:need:NONCE>one-sentence user goal</milk:need:NONCE>
when the user switches context, or on the first user message when no goal is set.
The stream parser intercepts and strips this tag (same pattern as percept tags).
Session.CurrentNeed is updated in-place; last value wins.
Budget configuration¶
context_budget_chars in ~/.milk/config.json (default: 12000). Applied
per brick (last_local_summary, last_claude_summary independently).
Consequences¶
- System prompt size at escalation is now bounded regardless of session length.
- Claude receives only what it doesn't already know: what the local model has done since Claude was last active.
- The
rapid_refill_breakerfailure mode is eliminated for normal sessions. - A session with a very long single local turn (e.g. a huge tool result as the most recent turn) may still approach the budget, but cannot exceed it.
last_claude_summaryis maintained but not yet used (pending demotion feature).- Existing sessions without brick fields load fine; bricks are rebuilt on the next turn.