DEV Community

Casey Sun
Casey Sun

Posted on

Kill the Merge When the Agent Invents Shared State

A brownfield checkout service sat behind two cache prefixes. An agent patch landed on a Friday. Staging returned 200 for every probe.

Monday traffic split across both prefixes. Orders hashed to the new key. Inventory still read the old one. The agent had invented a default, not a contract.

This article treats that pattern as assumption debt. It is a pre-merge kill list. Free model access and a free server can host the loop. They must not own shared state.

The failure is not a bad model

The model completed a plausible patch. Tests on a fresh database passed. Reviewers skimmed a large diff after lunch.

The damage lived in invented contracts. Cache keys. Column defaults. Migration names. Silent fallbacks. Those objects outlive the pull request.

Best-effort compute makes this worse. Retries appear. Partial tool calls land. The agent fills gaps instead of stopping.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. Some teams run disposable agent loops on MonkeyCode free model access and a free server option. That sandbox is the wrong owner for schema, cache, and dual-write paths.

A reconstructed Friday diff

The agent received a ticket about stale cart JSON. It never saw production key layout. It wrote a helper that looked tidy.

# agent-generated, do not merge as-is
CACHE_PREFIX = "cart:v2:"  # "v2" was not in the repo

def cart_key(user_id: str) -> str:
    return f"{CACHE_PREFIX}{user_id}"

def load_cart(cache, user_id: str) -> dict:
    raw = cache.get(cart_key(user_id))
    if raw is None:
        return {"items": [], "currency": "USD"}  # invented default
    return raw
Enter fullscreen mode Exit fullscreen mode

The old prefix was cart:user:. Currency lived on the account record. The empty cart hid a missed read. Dual-running code wrote two truths.

A later migration tried to help.

-- also agent-generated
ALTER TABLE carts ADD COLUMN currency TEXT DEFAULT 'USD';
CREATE INDEX CONCURRENTLY idx_carts_currency ON carts (currency);
Enter fullscreen mode Exit fullscreen mode

No rollback note existed. No dual-read window existed. The index locked a busy table. The default rewrote history for nulls.

Red flags before the agent touches state

Stop the loop when any item below is true. Do not negotiate with the patch.

  • The ticket does not name the current cache key.
  • Production still dual-reads two encodings.
  • The table has a backfill in flight.
  • The agent adds a column default on a live table.
  • The patch introduces a new environment variable.
  • Comments say assumed, should, or for now.
  • The free endpoint retried a write tool.
  • Tests spin an empty database only.

A free server is a sandbox. Shared state is not a sandbox object. Treat those as different classes of work.

Decision table: sandbox versus contract

Use this table in review. If the right column says kill, close the PR.

Change in the diff Allowed on free-loop sandbox Merge to shared systems
New unit test around pure functions Yes Yes
Prompt or tool-schema tweak Yes Yes, after golden tests
New cache key prefix Yes, throwaway namespace Kill until an allowlist names it
Column default or NOT NULL No Kill; needs a backfill plan
CREATE INDEX on a hot table No Kill; needs lock budget
Dual-write to two stores No Kill; needs an exit criterion
Invented currency, locale, or timezone No Kill; read the source of truth
Retryable payment or inventory tool No Kill; not a best-effort call

The table is the artifact. Code below only enforces it.

Sample harness: fail the job on invented contracts

Label: this is an unexecuted sample. Teams must wire it to their diff source. It reads a unified diff on stdin. It exits 1 on kill rules.

#!/usr/bin/env python3
"""pre_merge_kill_list.py — sample harness, not a measured benchmark."""
from __future__ import annotations

import re
import sys
from dataclasses import dataclass

ALLOWED_CACHE_PREFIXES = ("cart:user:", "session:v1:", "inv:sku:")
KILL_SQL = re.compile(
    r"\b(ALTER\s+TABLE|CREATE\s+INDEX|DROP\s+TABLE|DEFAULT\s+['\"]USD['\"])\b",
    re.I,
)
KILL_COMMENT = re.compile(r"\b(assumed|for now|should be|tmp prefix)\b", re.I)
CACHE_ASSIGN = re.compile(
    r"(CACHE_PREFIX|cache_prefix|key_prefix)\s*=\s*['\"]([^'\"]+)['\"]"
)
NEW_ENV = re.compile(r"os\.environ\[|os\.getenv\(")
INVENTED_DEFAULT = re.compile(
    r"default\s*=\s*['\"]USD['\"]|currency\s*[:=]\s*['\"]USD['\"]"
)

@dataclass(frozen=True)
class Hit:
    rule: str
    line: str


def added_lines(diff: str) -> list[str]:
    out: list[str] = []
    for line in diff.splitlines():
        if line.startswith("+++") or line.startswith("---"):
            continue
        if line.startswith("+") and not line.startswith("+++"):
            out.append(line[1:])
    return out


def scan(diff: str) -> list[Hit]:
    hits: list[Hit] = []
    for line in added_lines(diff):
        if KILL_SQL.search(line):
            hits.append(Hit("sql-contract", line.strip()))
        if KILL_COMMENT.search(line):
            hits.append(Hit("assumption-comment", line.strip()))
        if NEW_ENV.search(line):
            hits.append(Hit("new-env", line.strip()))
        if INVENTED_DEFAULT.search(line):
            hits.append(Hit("invented-default", line.strip()))
        m = CACHE_ASSIGN.search(line)
        if m and m.group(2) not in ALLOWED_CACHE_PREFIXES:
            hits.append(Hit("cache-prefix", line.strip()))
    return hits


def main() -> int:
    diff = sys.stdin.read()
    hits = scan(diff)
    if not hits:
        print("kill-list: clean")
        return 0
    print("kill-list: refuse merge")
    for hit in hits:
        print(f"  [{hit.rule}] {hit.line}")
    return 1


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

A tiny fixture keeps the rule honest. Save it next to the harness.

# test_pre_merge_kill_list.py — sample, not production evidence
from pre_merge_kill_list import scan

SAMPLE = """
--- a/cart.py
+++ b/cart.py
@@
+CACHE_PREFIX = "cart:v2:"
+return {"items": [], "currency": "USD"}
"""

def test_invented_prefix_and_default_are_kills():
    hits = {h.rule for h in scan(SAMPLE)}
    assert "cache-prefix" in hits
    assert "invented-default" in hits
Enter fullscreen mode Exit fullscreen mode

Run it against a real patch, not a story.

git fetch origin main
git diff origin/main...HEAD | python pre_merge_kill_list.py
python -m pytest test_pre_merge_kill_list.py -q
Enter fullscreen mode Exit fullscreen mode

Exit code 1 means the agent invented a contract. Humans rewrite the change. The sandbox may keep iterating on tests only.

Better alternatives after a kill

Do not prompt the same agent to try again. Change the work class.

  1. Freeze the contract in a checked-in allowlist.
  2. Write a characterization test against production fixtures.
  3. Dual-read both keys with a metric, never a silent fallback.
  4. Backfill in a tracked job with a row budget.
  5. Move writes off best-effort endpoints.

A dual-read sketch stays explicit. Missing keys must surface.

OLD = "cart:user:"
NEW = "cart:user:"  # same prefix until a named migration exists

def load_cart(cache, user_id: str):
    old = cache.get(OLD + user_id)
    new = cache.get(NEW + user_id)
    if old is None and new is None:
        raise KeyError(user_id)  # no invented empty cart
    if old is not None and new is not None and old != new:
        raise ContractDrift(user_id)
    return new if new is not None else old
Enter fullscreen mode Exit fullscreen mode

That function fails closed. Agents prefer fail-open helpers. Reviewers should reject fail-open helpers on shared paths.

Exit criteria for the free loop

Leave the free model loop when any criterion trips. Do not wait for an outage.

  • The patch touches migrations or cache prefixes.
  • A tool retry may have issued a write twice.
  • Golden fixtures from production are missing.
  • The allowlist must change to make CI green.
  • Reviewers cannot name the source of truth.
  • The job needs a latency or durability SLO.

After exit, continue on a paid, logged, single-writer path. Keep the sandbox for prompt tests. Keep the kill list in CI either way.

Who should not use this approach

Skip the harness theater in a few cases. It is the wrong control.

  • Greenfield apps with one writer and disposable data.
  • Pure documentation or comment-only diffs.
  • Teams without a named allowlist owner.
  • Work that already has a migration review board.
  • Any flow that must not send customer data to a free endpoint.

The kill list does not prove correctness. It only blocks a known bad class. It will miss renamed helpers. It will miss keys built by concatenation. It will miss SQL generated at runtime.

False kills will happen on legitimate defaults. Tune the allowlist in a separate PR. Never tune it inside the agent patch.

What the Friday patch needed

The ticket needed the existing prefix. It needed the account currency. It needed a miss to raise, not to invent USD. It needed no index on a Friday.

Free model access can draft the characterization tests. A free server can run those tests in isolation. Neither should mint cart:v2: or a column default.

Put the harness on stdin diffs first. Expand rules only after a real miss. Shared state stays human-owned until the allowlist says otherwise.

Top comments (0)