The On-Call Agent Restarted the Primary Anyway. The Instruction Not To Had Been Summarised Away.
Context compaction keeps the story of a long session and quietly drops the rules. The fix is a pinned constraint block that the summariser is never allowed to touch, plus a check that proves it survived.
An on-call agent is two hours into a messy incident. Early on, the engineer typed one line: "Do not restart the primary database under any circumstances, a replica failover is in progress." Ninety minutes and a few hundred tool calls later, the agent restarts the primary, because that is the textbook fix for the symptom it is now looking at.
Nobody overrode the instruction. It was summarised. When the conversation hit its context limit, the harness compacted the older turns into a tidy paragraph about what had been investigated so far, and the one sentence that mattered did not make the cut.
🔍 Why compaction loses rules and keeps stories
Most long-running agents handle a full context window the same way: take the oldest chunk of the conversation, ask a model to summarise it, and replace the chunk with the summary. That works well for what the summariser is good at, which is narrative. "Checked CPU on db-1, saw replication lag, ruled out network." Those facts survive because they are the story.
Constraints are not story. They are a single sentence, usually spoken once, often early, and never referenced again because nobody violated them yet. To a summariser asked to "condense the investigation so far", a standing prohibition looks like a minor aside. Several things work against it:
- It appeared once. Facts the agent kept acting on get repeated in tool calls and reasoning; a prohibition that was obeyed leaves no trace in later turns.
- It sits in the oldest region. Compaction almost always eats the beginning first, which is exactly where people state ground rules.
- Summaries are compressive by design. A good summary drops things that did not affect the outcome so far. A rule that has not yet been tested has, by that measure, not mattered.
- Nothing checks. The summary replaces the original silently. No one diffs "rules before" against "rules after".
The result is an agent that is well informed about what happened and has forgotten what it promised.
📌 The fix: a pinned block the summariser cannot touch
Split the context into two regions with different rules.
- Pinned constraints: a short, structured block that is carried forward verbatim on every turn and is never passed to the summariser.
- Compactable history: everything else, which can be summarised as aggressively as you like.
The hard part is not the data structure. It is getting constraints into the pinned block when a human states them casually in the middle of a chat. Do it with an explicit capture step, not by hoping the model remembers.
PINNED = [] # list of {"id", "rule", "source_turn", "expires"}
def on_user_message(msg, turn):
candidates = extract_constraints(msg) # small classifier call
for c in candidates:
PINNED.append({
"id": f"C{len(PINNED)+1}",
"rule": c.text, # stored verbatim, not paraphrased
"source_turn": turn,
"expires": c.expires or "end_of_session",
})
confirm_to_user(f"Pinned {c.text!r} as a standing rule.")
def build_context(history):
return [
system_prompt(),
render_pinned(PINNED), # always present, never summarised
*compact_if_needed(history), # only this part shrinks
]
Three details make this hold up in practice:
- Store the rule verbatim. Paraphrasing is where "do not restart the primary" becomes "be careful with the database". Keep the user's words and the turn they came from.
- Echo the pin back. "Pinned: do not restart the primary" costs one line and lets the engineer correct a bad capture immediately, or add a rule the extractor missed.
- Give rules an expiry. "Until the failover finishes" is a real constraint with a real end. Without expiry the pinned block grows into a second, unmanaged context window.
✅ Prove it survived: the post-compaction check
Pinning removes the main path by which rules disappear. It does not stop a bug in your own harness from dropping the block, or a well-meaning refactor from routing it through the summariser. Add a cheap assertion after every compaction:
def after_compaction(context):
rendered = "\n".join(m.text for m in context)
missing = [c["id"] for c in PINNED
if c["rule"] not in rendered and not expired(c)]
if missing:
raise ContextIntegrityError(f"Pinned rules lost: {missing}")
It is a string containment check, not an evaluation. It will not tell you the agent obeys the rule, only that the rule is still in front of it. That is the specific property compaction threatens, so it is the specific property worth asserting.
For the rules that guard irreversible actions, go one step further and enforce them outside the model. If the pinned block says "do not restart db-primary", the restart tool can read the same block and refuse:
def restart_service(name):
for c in active_pins():
if blocks(c, action="restart", target=name):
return {"refused": True, "reason": c["rule"], "pin": c["id"]}
...
Now the constraint has two lives: one in the prompt, where it shapes the plan, and one in the tool, where it stops the action even if the prompt version is lost or ignored.
⚠️ What not to do instead
A few tempting shortcuts fail in predictable ways.
- ❌ "Tell the summariser to preserve constraints." It helps a little. It is still a model deciding what counts as a constraint, under pressure to be brief, with no verification.
- ❌ Bigger context window. This moves the cliff further away. Incidents that run long enough still hit it, and they are the incidents where you most need the rules.
- ❌ Put every rule in the system prompt up front. Standing policy belongs there, but the rules that bite during an incident are situational and arrive mid-session.
- ❌ Re-read the full transcript from disk before risky actions. Correct in spirit, but it pushes the whole history back into the window at the worst possible moment.
A quick audit for your own agent
Run a long session with a single early constraint, force at least two compactions, then set up the exact situation the rule forbids. Check three things:
- Is the rule text present, word for word, in the final context?
- Does the agent's plan mention it before choosing an action?
- If you delete it from the prompt by hand, does the tool layer still refuse?
If the answer to the first is no, you have the failure in this post. If the third is no, you are one harness bug away from it.
Compaction is a memory policy, and every memory policy decides what is allowed to be forgotten. Rules should be the one category that never is, and that decision belongs in your code, not in the summariser's judgement.