A backend engineer boarded a regional train with a half-finished refactor and a local coding agent still running. The laptop remained warm, the working tree stayed dirty, and the cellular hop dropped packets every few seconds. The agent had a spill rule that moved heavy synthesis to a remote machine whenever local tokens felt slow. Mid-tunnel, that rule uploaded a truncated prompt, waited on retries, and returned a patch that no longer matched the files on disk.
That failure was not a model quality problem and it was not a missing unit test. It was a placement error that treated a brownout as spare remote capacity rather than as a damaged hop. Local-first agent work already holds the editor buffer, the test cache, and the secret material on the laptop. Relocating a turn across an unstable hop taxes latency, leaks partial context, and breaks offline continuity without buying useful compute.
The cost of relocating a turn on a flaky hop
A coding agent turn is a loop of reads, tool calls, and patches rather than a single completion. Each tool call on a remote worker pays wide-area latency twice, once outbound and once for the result. Packet loss converts those round trips into retries, and retries often resend prompts that now disagree with a tree that kept changing locally. Secrets in environment files, SSH keys, and session cookies should never ride that retry path, even toward a complimentary remote runner.
Offline work is the other constraint that remote spill quietly destroys during ordinary travel and building Wi-Fi. Elevators, rural stretches, and airplane mode still allow compilation, tests, and local model steps if no remote session starts. A free remote server wins only after the link is stable, the job is compute-heavy, and the payload has been stripped of secret-adjacent files. Until those three conditions hold together, the laptop should remain the only runtime for the agent loop.
Three connectivity classes
Teams can classify the current hop before any spill decision, using measurements anyone can reproduce on a laptop. The classes below are operational labels for placement, and they map onto latency, secrets, and offline behavior. They are not product tiers, and they should be computed immediately before a job is allowed to leave the machine.
- OFFLINE — name resolution fails, or several probes in a row time out with no useful payload.
- BROWNOUT — some probes succeed, but loss, jitter, or stall time would truncate a multi-tool agent turn.
- STABLE — consecutive probes succeed within a local budget, with jitter small enough for a full remote turn.
OFFLINE and BROWNOUT keep the agent on the machine that already has the working set. STABLE is permission to consider a free server for bulky, secret-free work, not an order to move every completion off the laptop.
A reproducible link-class probe
The following workflow stays on the developer machine and writes a small JSON record the agent policy can read. It does not require a vendor SDK, and it fails closed when the probe itself cannot run.
- Save the script in the repository as
scripts/link_class.pyso every clone measures the hop the same way. - Choose a boring probe target that is not the agent vendor, such as a public resolver or an internal status URL.
- Run a short burst of timed probes and record success ratio, mean delay, and delay spread for the hop.
- Map those three numbers onto OFFLINE, BROWNOUT, or STABLE with thresholds the team can argue about in review.
- Point the agent spill gate at the JSON file, and refuse relocation unless the recorded class is STABLE.
The script below is a labeled example for a POSIX laptop and should be treated as a starting point. Operators should tune thresholds against their own commute, office wireless, and VPN rather than copying constants blindly.
#!/usr/bin/env python3
"""Classify the current hop for local-first agent placement.
Labeled example: thresholds are starting points, not production SLOs.
"""
from __future__ import annotations
import argparse
import json
import socket
import statistics
import time
from pathlib import Path
def probe_once(host: str, port: int, timeout: float) -> float | None:
started = time.perf_counter()
try:
with socket.create_connection((host, port), timeout=timeout):
return (time.perf_counter() - started) * 1000.0
except OSError:
return None
def classify(samples: list[float | None], timeout_ms: float) -> str:
ok = [s for s in samples if s is not None]
if not ok:
return "OFFLINE"
loss = 1.0 - (len(ok) / len(samples))
mean = statistics.fmean(ok)
jitter = statistics.pstdev(ok) if len(ok) > 1 else 0.0
if loss >= 0.25 or mean > (0.6 * timeout_ms) or jitter > 80.0:
return "BROWNOUT"
return "STABLE"
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="1.1.1.1")
parser.add_argument("--port", type=int, default=443)
parser.add_argument("--samples", type=int, default=8)
parser.add_argument("--timeout", type=float, default=1.5)
parser.add_argument("--out", default=".agent/link_class.json")
args = parser.parse_args()
samples = [
probe_once(args.host, args.port, args.timeout) for _ in range(args.samples)
]
cls = classify(samples, args.timeout * 1000.0)
ok = [s for s in samples if s is not None]
record = {
"class": cls,
"successes": len(ok),
"attempts": len(samples),
"mean_ms": round(statistics.fmean(ok), 2) if ok else None,
"jitter_ms": round(statistics.pstdev(ok), 2) if len(ok) > 1 else None,
"allow_remote_spill": cls == "STABLE",
"keep_secrets_local": True,
}
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
print(json.dumps(record, indent=2))
if __name__ == "__main__":
main()
A wrapper in the agent bootstrap keeps the measurement close to the moment of placement rather than using a stale class.
#!/usr/bin/env bash
set -euo pipefail
python3 scripts/link_class.py --out .agent/link_class.json
python3 - <<'PY'
import json, sys
from pathlib import Path
rec = json.loads(Path(".agent/link_class.json").read_text())
if rec["class"] != "STABLE":
print(f"stay local: link class is {rec['class']}", file=sys.stderr)
sys.exit(10)
print("link class STABLE; remote spill is eligible for secret-free jobs")
PY
Exit code ten is a policy miss, not a crash, so wrappers can skip remote dispatch without failing local tests. A small unit test locks the classification rules so threshold tweaks cannot silently reopen remote spill.
# tests/test_link_class.py
"""Labeled example tests for the brownout classifier."""
from link_class import classify
def test_all_failures_are_offline():
assert classify([None, None, None, None], timeout_ms=1500) == "OFFLINE"
def test_high_loss_is_brownout():
samples = [40.0, None, 45.0, None, 42.0, None, 41.0, None]
assert classify(samples, timeout_ms=1500) == "BROWNOUT"
def test_tight_cluster_is_stable():
samples = [18.0, 19.0, 17.0, 20.0, 18.5, 19.5, 18.0, 19.0]
assert classify(samples, timeout_ms=1500) == "STABLE"
PYTHONPATH=scripts pytest tests/test_link_class.py -q
Placement table after the class is known
Once the JSON record exists, placement becomes a table that reviewers can demand beside probe output. Latency, secret residency, and offline continuity stay explicit instead of hiding inside a remote SDK default.
| Link class | Interactive edits | Long test or build | Secret files | Offline expectation |
|---|---|---|---|---|
| OFFLINE | Local agent only | Local agent only | Never leave disk | Continue; queue remote work |
| BROWNOUT | Local agent only | Local agent only | Never leave disk | Continue; do not open a remote session |
| STABLE | Prefer local for tight loops | Free server may win | Still local or redacted | Remote allowed for bulky jobs |
Interactive turns remain on the laptop even on a STABLE link because keystroke-coupled tool calls are latency sensitive. A free server becomes interesting for long, parallel, secret-free jobs after the hop has earned that class. The table belongs in review next to the JSON record, so spill decisions leave an auditable trail.
When the free server is the right runtime
Local-first policy is not a vow to ignore spare remote capacity when the laptop is already thermally saturated. A STABLE hop plus a job that thrashes memory, fans, or battery is a legitimate case for remote execution. Useful examples include a full integration suite, documentation synthesis over committed trees, and batch refactors without editor round trips. Those jobs still ship a scaffold without environment files, credentials, or uncommitted secrets, which keeps residency honest.
Limitations
The socket probe measures TCP reachability, not application throughput, TLS interception, or a captive portal page. A hotel network that accepts a TCP connect and then injects a login page can still look STABLE to this script. VPN split tunnels can mark a public address stable while the intended runner remains unreachable behind another interface. Production gates should probe the actual spill endpoint, and fiber-tuned thresholds will misclassify cellular handoffs during travel.
The script does not observe battery level, thermal throttle, or a dirty working tree on disk. The classifier also cannot see truncated prompts after a remote session has already started on a fading hop. A job that begins STABLE and then enters a tunnel still needs a cancel-and-resume path on the laptop. Without that path, brownout placement only helps at job start and cannot repair a session already in flight.
Who should not use this gate
Engineers on wired office networks with a single always-on runner gain little from eight extra probes per turn. Safety-critical systems that already forbid any remote model should not add a STABLE branch that reintroduces egress. Teams without a secret-redaction step should keep every agent turn local, because a green class does not sanitize payloads. If the local machine cannot run tests at all, fixing the local toolchain matters more than classifying the hop.
The train story ends the same way most brownouts end for people who keep a working toolchain on the laptop. The useful work was the local compile and the tests that never needed a remote hop to finish. Classify the link, keep secrets on disk, and relocate an agent job only when the hop is STABLE and the job is actually large.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode provides free models that can run this workflow.
Top comments (0)