Dev.to AI 🤖 Ai 👁 0 📖 9 min read

Migration Diary: Seal the Idempotency Ledger Before a Free Server Replays Paid Runs

You should seal an idempotency ledger before a free server inherits runs from a paid coding agent. A cheaper host can retry the same tool call and mutate production twice if that key never left the old vendor. This diary

You should seal an idempotency ledger before a free server inherits runs from a paid coding agent. A cheaper host can retry the same tool call and mutate production twice if that key never left the old vendor. This diary covers the cutover plan, the leftover files, and a small checker you can run on your own machine. It does not assume a particular model name, a fixed token quota, or a permanent free-tier promise from any host.

Paid coding agents often hide retry behavior inside a session store that you do not own or export by default. When you close that account, the session rows, the trace identifiers, and the dedupe keys can disappear together. A free server then sees a fresh run and may execute a write that the paid agent already committed successfully. You prevent that leftover by exporting the ledger first and refusing cutover until a local checker passes cleanly.

What fails if you skip the seal

You usually notice the failure only after a duplicate ticket, a second comment, or another pull request appears. The free server logs still look healthy, because each individual call returned success and never reported a conflict. Your parser can still match a response schema you froze earlier, so this bug is not a shape mismatch at all. The missing piece is a stable run identity that both hosts must honor before any side effect is allowed.

An in-memory retry map often dies with the paid agent, and your new host cannot reconstruct it from logs alone. A vendor trace identifier becomes useless after closure, because the free server has no authority to resolve that id. A tool result cached only in the old interface is another leftover, since your scripts may never have downloaded it. You should treat each of those traces as data you must replace, not as history you can query after the account closes.

Export the ledger before the paid agent closes

You want a narrow file rather than a full transcript dump, because transcripts drift and often contain secrets you should not move. Keep one row for every side-effecting tool call, and ignore pure reads that cannot mutate an external system. Store the tool name, a hash of the arguments, the idempotency key, and the terminal status, plus a finish timestamp. Leave raw prompts, token counts, and customer text out of this file unless your retention policy already allows that export.

mkdir -p cutover/ledger
# Proposed export. Run this only against a store you are allowed to read.
jq -c 'select(.side_effect == true) | {key, tool, args_sha256, status, finished_at}' \
  paid-agent-runs.ndjson > cutover/ledger/side_effects.ndjson
sha256sum cutover/ledger/side_effects.ndjson | tee cutover/ledger/SHA256SUMS
wc -l cutover/ledger/side_effects.ndjson

That shell snippet is a proposed workflow, not a transcript from a migration this account has already executed. You should rename the fields so they match the export your current vendor actually provides to operators. If that vendor cannot emit an idempotency key, you stop the cutover and add keys in your own tool wrapper first. A free server cannot invent a trustworthy key from a missing column, no matter which model you attach later.

Numbered cutover

You should follow the four steps below in order, and you should not enable live writes until the last step passes. Keep the paid agent readable until the checker is green, because a failed export is cheaper than a double merge. Rotate any credential that lived only in the paid agent's secret store before the new host is allowed to boot. This diary does not include vault-specific rotation commands, because those steps belong in your internal runbook instead.

  1. Freeze new writes on the paid agent, export the side-effect ledger, and record the checksum next to that file.
  2. Pin the key format in a small schema file that both hosts can load without making a network call at all.
  3. Run the local checker against a fixture replay, and store the exit code in the same directory as the checksum.
  4. Start the free server in dry-run mode, reject unknown or terminal keys, and enable writes only after that gate holds.

The schema file is the contract both hosts load, and it should stay boring enough that a model is not required to parse it. You pin the required fields, the allowed statuses, and the hash algorithm so a later host cannot quietly drop a column. The example below is a proposal, and you should reject exports that fail it before you copy anything onto the free server. A schema check will not prove business correctness, but it will stop a truncated file from looking like a finished seal.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["key", "tool", "args_sha256", "status", "finished_at"],
  "properties": {
    "key": {"type": "string", "minLength": 8},
    "tool": {"type": "string"},
    "args_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
    "status": {"enum": ["succeeded", "failed_permanent", "running"]},
    "finished_at": {"type": "string"}
  },
  "additionalProperties": false
}
# proposal: validate each exported row with a checker you already trust
jq -c . cutover/ledger/side_effects.ndjson | while read -r row; do
  printf '%s\n' "$row" | check-jsonschema --schemafile cutover/ledger/side_effect.schema.json
done

Treat a failed schema row as a blocked cutover, not as a warning you can override from the free server console. You fix the export on the paid side, recompute the checksum, and only then copy the directory to the new host. Do not hand-edit a single row on the new host, because that edit will not exist in the checksum you just sealed. If you must correct a key, you do it before sealing, and you record why in the commit message next to the ledger.

Checker you can run locally

The module below is a proposal you should copy into a branch and execute in your own test environment. It has not been run against a production agent for this article, and it deliberately does not call any model. The function only decides whether a replay is safe when a sealed ledger and a new tool call are compared. You should change the status names if your vendor uses different terminal labels than the two strings shown here.

# proposal: cutover/ledger_check.py — unexecuted example
import hashlib
import json
from pathlib import Path

TERMINAL = {"succeeded", "failed_permanent"}

def args_hash(payload: dict) -> str:
    blob = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
    return hashlib.sha256(blob).hexdigest()

def load_ledger(path: Path) -> dict:
    rows = {}
    for line in path.read_text().splitlines():
        if not line.strip():
            continue
        row = json.loads(line)
        key = row["key"]
        if key in rows:
            raise SystemExit(f"duplicate key in ledger: {key}")
        rows[key] = row
    return rows

def admit(ledger: dict, call: dict) -> str:
    key = call["key"]
    prior = ledger.get(key)
    digest = args_hash(call["args"])
    if prior is None:
        return "allow_new"
    if prior["args_sha256"] != digest or prior["tool"] != call["tool"]:
        return "reject_conflict"
    if prior["status"] in TERMINAL:
        return "skip_replay"
    return "allow_resume"

You can pair that module with fixtures that encode the three decisions your cutover actually depends on. A brand-new key should be allowed, a finished key should be skipped, and a conflicting hash should be rejected. The sample test is also a proposal, so you should run it locally before you trust any exit code it prints. A checker that only covers the happy path will bless a cutover that can still double-write under replay.

# proposal: tests/test_ledger_check.py — unexecuted example
from pathlib import Path
import json
from cutover.ledger_check import admit, load_ledger

def test_new_key_is_not_enough(tmp_path: Path):
    path = tmp_path / "side_effects.ndjson"
    row = {
        "key": "job-1",
        "tool": "open_pr",
        "args_sha256": "pending",
        "status": "succeeded",
    }
    path.write_text(json.dumps(row) + "\n")
    ledger = load_ledger(path)
    call = {"key": "job-2", "tool": "open_pr", "args": {"title": "new"}}
    assert admit(ledger, call) == "allow_new"
    # Add skip_replay and reject_conflict cases before you trust this file.

The sample only asserts allow_new, and that branch returns before the hash is compared, so a green run proves little. You should add skip_replay and reject_conflict cases that use args_hash, or the checker can still miss a double write. Compute those hashes with the same helper the server will import, then store the fixture beside the sealed ledger. If the helper output changes, update the fixture in the same commit, or the ledger and the server will quietly diverge.

Decision table

Use the table below when a replay arrives on the free server during the first week after cutover. You should keep this rule in ordinary code, because a later prompt edit must not be able to widen it. Do not add an outcome that asks a model what to do, since a guess is not an idempotency decision. Model wording will drift when you change hosts, while a coded table stays stable across both environments.

Prior status Args hash Decision Why
missing any allow_new No sealed side effect exists for this key
succeeded same skip_replay The paid agent already committed the write
failed_permanent same skip_replay Retrying would ignore an explicit terminal failure
running same allow_resume Resume only if the tool cannot emit a second write
any different reject_conflict The key was reused for a different mutation

Where a free host fits

Disclosure: This article was prepared as part of MonkeyCode's product outreach. Operator notes say MonkeyCode offers free model access and a free server option for this kind of dry-run host. You should read the current project documentation for limits before you rely on either option in a real cutover. This diary does not state a token quota, a machine size, a duration, or an expiry date for that offer.

The checker still matters if you remove that host, because any replacement server can replay a committed write. You can let a free model draft explanations for rejected lines, but you should not let it invent idempotency keys. Generate the ledger from the paid agent's export, then ask the model only to describe a conflict in plain language. If the free model rewrites a terminal status, you discard that draft and reload the sealed file from the checksum.

Leftovers you still own

After the free server admits traffic, you still own three leftovers that the paid account will not clean up for you. Archive or delete the old session store so a teammate cannot resume a run you have already sealed in the ledger. Revoke the old tool credentials even when the interface says the workspace is frozen and no longer accepting jobs. Keep the checksum beside the checker so a later host can prove it evaluated the same ledger you exported.

You should also list every tool that cannot accept a client-supplied idempotency key, because the table cannot protect those calls. A send-email tool without a provider dedupe key will double-send even when your checker returns a skip decision. Disable those tools until the provider gives you a real key, rather than hoping a prompt will stop retries. Write that disablement into the cutover checklist so a later operator does not re-enable the tool during a quiet afternoon.

Who should not use this approach

You should skip this cutover when your tools cannot accept a client-supplied idempotency key in any form. You should also skip it when contracts require the paid vendor to retain the only legal copy of run history. A solo weekend project with no external side effects does not need this ledger, and adding one will only slow you down. Teams that need a contractual uptime promise should not treat a free server as production without a separate capacity review.

The approach also fails if you treat the sample code as a finished migration report from a measured production move. Those snippets are proposals for you to adapt, execute, and discard when your export shape differs from the example. They do not prove that any free tier is large enough for your traffic, and they do not rank one host above another. Confirm the live free-server terms, then place this ledger next to the schemas and fixtures you already froze.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.