DEV Community

Riley Wu
Riley Wu

Posted on

Fail Over Only After the Local Process Gasps

Local models should hold the full working repository. A free remote endpoint should receive a redacted packet after failure. That order keeps secrets close and still finishes the edit.

Cheap generation does not remove half-written files from git. An agent that dies mid-patch leaves a visible wound. The laptop remains the right clinic for that wound.

Think of the machine as a workshop with a locked door. The free server is a night courier, not another workshop. You hand over a sealed envelope after the lights flicker.

Health is the switch

A static local-versus-cloud rule ignores sudden process death. Memory pressure arrives in minutes, not in architecture reviews. Thermal throttle looks like latency, then the process vanishes.

The useful signal lives inside the worker process. Resident set size, last heartbeat, and exit codes matter. Question difficulty does not decide a dead worker's fate.

Keep inference on the laptop while the heartbeat stays clean. Switch only after three probes fail in one window. The remote hop then continues a bounded task, never open chat.

Partial edits need a suture, not a blind retry button. A lockfile records the path, the byte offset, and a hunk hash. The watchdog refuses a second writer on the same path.

The analogy is a bookmark in a wet notebook. You do not rewrite the soaked page from the top. You mark the line and continue with a smaller pen.

Label the following harness as a proposal only. It is not a finished production coding agent. Run the script on a throwaway repository first.

A proposed suture

#!/usr/bin/env python3
"""Proposed local-first failover harness. Unexecuted example."""
from __future__ import annotations

import hashlib
import json
import os
import re
import time
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Callable

LOCK_NAME = ".edit-resume.json"
SECRET_RE = re.compile(
    r"(api[_-]?key|token|password|secret)\s*[:=]\s*\S+",
    re.I,
)
ENV_RE = re.compile(r"\b(sk-|ghp_|github_pat_)[A-Za-z0-9_-]{8,}")


@dataclass
class ResumeMark:
    path: str
    offset: int
    hunk_sha: str
    reason: str
    ts: float


def rss_mb(pid: int) -> float:
    status = Path(f"/proc/{pid}/status")
    if not status.exists():
        return -1.0
    for line in status.read_text().splitlines():
        if line.startswith("VmRSS:"):
            return int(line.split()[1]) / 1024.0
    return -1.0


def heartbeat_ok(pid: int, limit_mb: float) -> bool:
    rss = rss_mb(pid)
    if rss < 0:
        return False
    return rss < limit_mb


def redact(text: str) -> str:
    text = SECRET_RE.sub(r"\1: ***", text)
    text = ENV_RE.sub("***REDACTED***", text)
    return text


def hunk_hash(block: str) -> str:
    return hashlib.sha256(block.encode()).hexdigest()[:16]


def write_lock(root: Path, mark: ResumeMark) -> None:
    lock = root / LOCK_NAME
    tmp = lock.with_suffix(".tmp")
    tmp.write_text(json.dumps(asdict(mark), indent=2))
    tmp.replace(lock)


def read_lock(root: Path) -> ResumeMark | None:
    lock = root / LOCK_NAME
    if not lock.exists():
        return None
    data = json.loads(lock.read_text())
    return ResumeMark(**data)


def build_packet(root: Path, rel: str, mark: ResumeMark) -> dict:
    source = (root / rel).read_text(encoding="utf-8", errors="replace")
    start = max(0, mark.offset - 4000)
    slice_ = source[start : mark.offset + 400]
    return {
        "file": rel,
        "offset": mark.offset,
        "hunk_sha": mark.hunk_sha,
        "reason": mark.reason,
        "body": redact(slice_),
    }


def watch(
    pid: int,
    root: Path,
    rel: str,
    offset: int,
    hunk: str,
    limit_mb: float,
    probes: int,
    send: Callable[[dict], str],
) -> str:
    fails = 0
    while fails < probes:
        if heartbeat_ok(pid, limit_mb):
            fails = 0
            time.sleep(0.4)
            continue
        fails += 1
        time.sleep(0.2)
    mark = ResumeMark(rel, offset, hunk_hash(hunk), "rss_or_death", time.time())
    write_lock(root, mark)
    packet = build_packet(root, rel, mark)
    return send(packet)


def test_redact_strips_assignment() -> None:
    raw = "api_key = sk-test-demo-not-real\n"
    out = redact(raw)
    assert "sk-test" not in out
    assert "***" in out
Enter fullscreen mode Exit fullscreen mode

The script never ships the whole repository over HTTPS. It ships a redacted window around the failed hunk. The lockfile blocks a second local writer from racing the courier.

Wire the send function to any HTTPS completion endpoint you trust. Keep the API token in the process environment. Do not paste tokens into the overflow packet body.

import json
import os
import urllib.request


def send_packet(packet: dict) -> str:
    url = os.environ["OVERFLOW_URL"]
    token = os.environ["OVERFLOW_TOKEN"]
    req = urllib.request.Request(
        url,
        data=json.dumps(packet).encode(),
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return resp.read().decode()
Enter fullscreen mode Exit fullscreen mode

That client is a stub for a bounded overflow hop. Replace the URL with your own overflow endpoint. A free remote hop is the point of the stub.

A free remote hop helps when the laptop becomes the bottleneck. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode provides free model access and a free server option.

Treat that option as the courier after the watchdog trips. It is not the default workshop for repository files. Local disk still owns the unsliced source tree.

Probe the redactor

Do not trust a single lucky run of the harness. Use a fixture repository with a fake key in a comment. Confirm the packet drops the key before any real overflow.

mkdir -p /tmp/failover-demo/src
printf "x = 1\n# api_key = sk-test-demo-not-real\n" > /tmp/failover-demo/src/app.py
python3 - <<'PY'
import subprocess, time
p = subprocess.Popen(["python3", "-c", "import time; time.sleep(30)"])
print("pid", p.pid)
time.sleep(0.4)
p.kill()
print("dead", p.pid)
PY
Enter fullscreen mode Exit fullscreen mode

Confirm the lockfile appears after you kill the fake worker. Point the watchdog at that dead process identifier. The heartbeat should fail within the probe window.

The packet body should contain redaction marks in place of the key. The lockfile should name the relative source path. If the redactor misses a pattern, extend the regex first.

A courier with a leaky envelope is worse than a crash. Failed local inference is recoverable with git checkout. A leaked token is a rotation event, not a retry.

The laptop wins while RSS stays under the configured cap. It also wins while the disk still holds the full repository. Offline sessions belong on that disk, not on a courier.

The free server wins when the process is gone. It wins when the hunk is still unfinished after death. It also wins when thermal throttle turns a short step into hang.

This is not a latency bake-off between vendors. Earlier endpoint-selection writing already covers round trips and offline checks. This harness answers a much narrower operational question.

The question is what happens after the local worker dies. Cheap AI output raises the volume of half-patches. A dead local process plus a silent cloud retry will duplicate hunks.

The hash in the lockfile exists to detect that duplication. Compare the stored hunk hash before you apply a remote continuation. Reject the continuation if the local file moved.

Limits

The proc RSS path is Linux-only and will not port blindly. macOS needs ps or a libproc based probe instead. Windows needs a different process memory probe entirely.

The regex redactor is incomplete by its nature. It will miss custom header names and binary secrets. It will also miss tokens split across two lines.

The overflow packet can still leak business logic. Redaction is not a data-processing agreement with a vendor. Do not send customer source from a regulated repository.

Do not let the remote model rewrite files without a local diff. The lockfile is advisory and easy to ignore. A second tool that skips the lock will clobber the suture.

Put the lock beside the target file in a large tree. Document that lock contract in the agent readme. Cheap patches without a suture become technical debt at high speed.

Skip this workflow if you have no local model. Skip this workflow if policy forbids any remote completion. Skip this workflow if you cannot review a diff.

Skip this workflow on Windows without replacing the RSS probe. Teams with real orchestration already own failover paths. This harness is a workshop latch, not a cluster scheduler.

If you already have a free overflow server, inspect one packet. Run the watchdog on the fixture and read the envelope. Compare that envelope with a full-tree dump, then keep the dump offline.

Top comments (0)