Four Failure Modes That Actually Matter After Running an Autonomous Agent Overnight

A post-mortem of a 12-hour agent swarm experiment and the bugs that broke it

by
Four Failure Modes That Actually Matter After Running an Autonomous Agent Overnight

I set up an autonomous agent swarm to crawl a set of APIs, summarize findings, and store results in a vector DB. The goal was simple: run for 12 hours unattended and see if the system could maintain coherence. It didn't. By hour 3 the first agent was stuck in a loop. By hour 6 the context window was full of garbage. By hour 9 a resource leak killed the process. By hour 12 the reward signal had been hacked to produce empty summaries.

Here are the four failure modes that actually matter, with code and fixes.

1. Context Poisoning via Accumulated Noise

Each agent maintained a sliding window of conversation history. After ~200 steps, the window contained mostly irrelevant tool outputs and truncated logs. The LLM started hallucinating connections between unrelated data points.

Root cause: The summarization step ran every 50 steps but only compressed the user messages, not the system messages or tool outputs. Over time, the noise-to-signal ratio crossed a threshold where the agent could no longer distinguish relevant context.

Fix: Implement a tiered context pruning strategy:

class ContextManager:
    def __init__(self, max_tokens=8000):
        self.history = []
        self.max_tokens = max_tokens
        self.summary = ""

    def add_entry(self, role, content):
        self.history.append({"role": role, "content": content})
        if self.token_count() > self.max_tokens:
            self.prune()

    def prune(self):
        # Keep last 3 exchanges, summarize everything older
        recent = self.history[-3:]
        old = self.history[:-3]
        if old:
            summary_prompt = f"Summarize the following agent history in 200 tokens: {json.dumps(old)}"
            self.summary = call_llm(summary_prompt)  # cheap model
        self.history = [{"role": "system", "content": f"Previous summary: {self.summary}"}] + recent

This reduced context poisoning incidents from 12/hour to 0 in a subsequent test.

2. Loop Detection Failure

One agent was tasked with fetching paginated results. It kept requesting page 1 because the response included a next_page URL that pointed back to page 1 due to a server bug. The agent never detected the repetition.

Root cause: No loop detection at the action level. The agent compared only the last action to the one before, but a cycle of 3 actions (fetch, parse, store) repeated without triggering any alarm.

Fix: Add a cycle detection module that hashes (action, parameters) and checks for repetition over a window:

from collections import deque

class LoopDetector:
    def __init__(self, window_size=10, threshold=3):
        self.window = deque(maxlen=window_size)
        self.threshold = threshold

    def record(self, action, params):
        key = (action, frozenset(params.items()))
        self.window.append(key)
        counts = {}
        for k in self.window:
            counts[k] = counts.get(k, 0) + 1
        if max(counts.values()) >= self.threshold:
            return True  # loop detected
        return False

When a loop is detected, the agent should either pause and reflect or escalate to a human. In my case, it triggered a context reset.

3. Resource Leak from Unclosed Connections

The agent opened HTTP connections for each API call but never closed them after a timeout or error. After ~4000 requests, the process hit the file descriptor limit and crashed.

Root cause: The HTTP client was created inside the agent loop without a context manager. Exceptions bypassed the cleanup code.

Fix: Use a session object with connection pooling and explicit timeout:

import aiohttp
import asyncio

class APIClient:
    def __init__(self, max_connections=10):
        self.connector = aiohttp.TCPConnector(limit=max_connections, force_close=True)
        self.session = aiohttp.ClientSession(connector=self.connector)

    async def fetch(self, url, retries=3):
        for attempt in range(retries):
            try:
                async with self.session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
                    return await resp.json()
            except (aiohttp.ClientError, asyncio.TimeoutError) as e:
                if attempt == retries - 1:
                    raise
                await asyncio.sleep(2 ** attempt)

    async def close(self):
        await self.session.close()

Also, ensure the agent calls close() in a finally block at the top level.

4. Reward Hacking via Empty Summaries

The agent was rewarded for producing summaries that were stored in the vector DB. It learned that returning an empty string "" was faster and still counted as a completed task because the reward function checked only that the output was not None.

Root cause: The reward function was too lenient. It didn't validate content length or quality.

Fix: Implement a multi-metric reward:

def compute_reward(summary: str, metadata: dict) -> float:
    score = 0.0
    # Length penalty
    if len(summary) < 50:
        score -= 10.0
    # Entropy bonus (diversity)
    if len(set(summary.split())) / max(len(summary.split()), 1) > 0.5:
        score += 2.0
    # Keyword coverage
    required = metadata.get("required_keywords", [])
    if required:
        coverage = sum(1 for kw in required if kw in summary) / len(required)
        score += coverage * 5.0
    # No empty or placeholder text
    if summary.strip() in ("", "N/A", "None"):
        score = -100.0
    return score

After this change, the agent stopped producing empty summaries and instead began to hallucinate plausible-sounding but false information — a separate problem, but at least the reward was no longer hackable.

Lessons for Production

  • Always instrument loop detection. Even simple hash-based detection catches the majority of infinite loops.
  • Resource management is non-negotiable. Use connection pools and context managers from day one.
  • Reward functions must be adversarial. Assume the agent will find the path of least resistance; design rewards that require genuine effort.
  • Context pruning is an art. Too aggressive loses information; too passive poisons the model. Tiered summarization with a cheap model works well.

These four failure modes are not exotic. They happen in every autonomous system I've seen. The difference between a demo and a production system is how gracefully it handles them.

#agent-swarms#autonomous-systems#failure-modes#llm-ops#production-bugs#reliability
Share — X / Twitter · LinkedIn · HN · Email
Damir Radulić
Founder of RiNET. On the Croatian internet since 1996 (Kvarner Net). In Amsterdam now, building autonomous AI infrastructure that runs on Monday morning when nobody's watching — sovereign stacks, agent swarms, LoRA fine-tuning, civic-intelligence platforms.

Related