DEV Community

Sam Yang
Sam Yang

Posted on

The Vanished Constraint: Debugging a Coding Agent That Forgot Its Own Decision

A few weeks ago I watched a coding agent reintroduce a bug that the team had spent a month removing from a payment service. The assigned task was straightforward: extract the rate limiter into its own module, and the first diff looked clean enough to merge without a second review. The tests passed and the type checker stayed quiet, but the load test collapsed at a fraction of the usual traffic with a stack trace pointing at a process-wide lock that had been deliberately deleted months earlier. The details are a composite of several similar incidents, but the pattern is common enough that you have probably seen it yourself.

The agent had not been careless, and it had not ignored the instructions; it had simply lost an earlier decision when its context window filled up. The conversation summarizer compressed the hard constraint "never use a global lock in this path" into the vague phrase "be careful with concurrency", and the model dutifully read that as permission to add a lock. This is the failure mode I want to dissect here, because it is reproducible, measurable, and fixable with a small harness that costs almost nothing to run.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. The workflow below is designed around MonkeyCode's free model access and free server option, because both make repeated replay testing affordable in a way that paid APIs do not. The often-quoted ten-million-token allowance is exactly the kind of number you should verify in the project repository before you build a workflow around it.

Treat context loss as a memory-system bug rather than a model quality problem, and the debugging path becomes obvious. A teammate who joins a meeting halfway through will make different decisions than one who sat through the opening argument, and no amount of intelligence fixes missing context. The same logic applies to an agent whose early turns have been summarized away, so the first question is not which model is smarter but where the constraint disappeared.

The reproduction step needs three ingredients: a fixed task description, a fixed conversation history, and one constraint that must survive compression. Pick a constraint your team actually cares about, such as a banned function call or a forbidden import, and run the agent twice against the same task with different history lengths. Record the diffs, and you will often see the exact turn where the constraint stops influencing the output.

The isolation step is where the technique becomes genuinely useful, because you can bisect the conversation the way you would bisect a failing commit. Replay the task with the first half of the turns, then the first quarter, and keep halving until you find the shortest history that still produces the banned pattern. That boundary is your agent's effective memory horizon, and it will differ across models, providers, and even prompt formats.

The fix is to externalize the decision so that compression cannot destroy it, which is where a decision ledger earns its keep. Create a small file with machine-readable markers, and instruct the agent to read it before touching any file in the affected scope:

<!-- decision: no-global-lock -->
Reason: process-wide lock caused a deadlock incident.
Scope: payment_service/*.py
Enforced by: constraint_regression.py
Enter fullscreen mode Exit fullscreen mode

The marker is deliberately boring, because a plain sentence can survive summarization better than a nuanced paragraph buried in a long conversation. Once the ledger exists, you can encode the constraint as a regression test that runs on every agent output, and the harness below does exactly that.

#!/usr/bin/env python3
"""constraint_regression.py — replay a coding-agent task with two history
lengths and fail if a banned pattern reappears in the produced patch."""

import argparse
import re
import subprocess
import sys
from pathlib import Path

BANNED_PATTERNS = [
    (r"payment_service/.+\.py", r"threading\.Lock"),
    (r"payment_service/.+\.py", r"asyncio\.Lock"),
]

def run_agent(command: list[str]) -> str:
    result = subprocess.run(command, capture_output=True, text=True, timeout=900)
    if result.returncode != 0:
        print(result.stderr, file=sys.stderr)
    return result.stdout

def check_diff(diff: str) -> list[str]:
    violations = []
    for path_pattern, code_pattern in BANNED_PATTERNS:
        if re.search(path_pattern, diff) and re.search(code_pattern, diff):
            violations.append(f"{code_pattern} in a matching hunk")
    return violations

def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--cmd", nargs="+", required=True,
                        help="agent CLI, e.g. --cmd monkeycode run --task task.md")
    parser.add_argument("--full-history", type=Path)
    parser.add_argument("--truncated-history", type=Path)
    args = parser.parse_args()

    failed = 0
    for label, history in (("full", args.full_history),
                           ("truncated", args.truncated_history)):
        command = list(args.cmd)
        if history:
            command += ["--history", str(history)]
        diff = run_agent(command)
        violations = check_diff(diff)
        print(f"[{label}] violations: {violations}")
        failed += bool(violations)
    return 1 if failed else 0

if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

Run the harness once with the full conversation and once with the truncated one, and the exit code tells you whether your agent still honors the constraint after compression. The script is deliberately agent-agnostic, so it does not care whether the backend is a hosted model or MonkeyCode's free server option on your own machine. That last point matters for teams that cannot send proprietary code to a third-party API, because a self-hosted endpoint keeps the repro entirely inside your network.

The technique has real limits, and you should know them before you adopt it. Pattern matching catches renamed locks only if you add the new name to the banned list, and semantic regressions that use a different synchronization primitive will slip through entirely. Truncation behavior varies across models and providers, so a repro that fails on one free model may pass on another, which is useful for comparison but dangerous for drawing permanent conclusions. The ledger also depends on the agent actually reading it, because under extreme compression even system prompts get summarized, and no marker survives a model that never sees it.

You should skip this workflow if your tasks fit comfortably inside one context window, because the harness is overhead when nothing ever gets truncated. You should also skip it if your team has no history of context-loss bugs, or if you cannot afford the replay time that makes the bisection meaningful. But if you have ever merged a diff that looked right and then spent a day explaining why it was wrong, this is a cheap way to turn that memory into a regression test; the script runs against any agent CLI, and MonkeyCode's repository is the right place to confirm current token allowances and server details before you try it.

Top comments (0)