A mid-afternoon refactor on a payments service looked simple until the agent started walking the repository one search at a time. The first rg finished locally in forty milliseconds, but the next step waited on a remote hop of two hundred milliseconds. Twelve more searches, two test invocations, and one format pass stacked those hops into a multi-second pause that felt like a hang. A dotenv file in the same tree made a naive move onto a bigger remote box unsafe for that session.
Serial tool calls punish remote placement
Local-first agent loops are not slow because local processors cannot search an ordinary application tree. They feel slow when every observation waits on a model that lives one network round trip from the working copy. Chatty sessions issue many short tool calls in sequence, so latency adds in series instead of hiding under one compile. A remote placement that looks cheaper on tokens can still lose wall-clock time once hop counts climb into double digits.
Interactive loops also keep secrets in prompt context without anyone intending to export those values off the laptop. Path names, stack traces, and failed test output often carry hostnames, tokens, and customer identifiers from local fixtures. Spilling that transcript to a free server to chase speed trades a latency problem for an egress problem that is harder to unwind. Offline windows make the same trade worse, because a job that already left the laptop cannot continue when the commute tunnel drops.
Three vetoes that are not one score
Latency, secret residency, and offline continuity should not be mashed into a single heuristic score for placement. A low round-trip time does not authorize shipping a private key material file to any remote worker. A clean secret scan does not help if the laptop is about to close the lid on a train. A stable network does not justify remote placement when the session is a tight loop over a dirty tree.
The table below is a proposed decision aid, not a production policy and not a measured benchmark from a live fleet.
| Shape of work | Typical hop count | Secrets in tree | Link state | Placement |
|---|---|---|---|---|
| Chatty search and test loop | 8+ serial tools | present or unknown | any | stay local |
| Chatty loop, fixtures only | 8+ serial tools | none found | online | stay local anyway |
| One-shot lint or format | 1–2 tools | none found | online | local preferred |
| Long test or compile batch | 1–3 coarse steps | none found | online and stable | free server may win |
| Long batch with mixed files | any | secret-shaped hits | any | stay local |
| Any job during a drop | any | any | offline or brownout | stay local |
The chatty rows are the ones teams skip when a bigger remote box looks tempting on paper. Chatty loops stay local even when the secret scanner is quiet, because the round-trip tax dominates wall-clock time. The free-server win is the coarse batch that does not need a secret and does not need a reply between every file touch. Remote almost never wins the millisecond comparison on serial hops; it wins when the job is coarse enough that CPU, not chatter, dominates.
When a job is batch-shaped, secret-clean, and online, a free server can absorb a heavy compile without holding the interactive loop hostage. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option fit that batch path only after the hop gate says the working set may leave. Readers who already trace local agent sessions can point the same gate at that free-server path and keep the chatty loop on the laptop.
A proposed hop-budget artifact
The script below is an unexecuted example for a laptop-side preflight before any agent placement change. It reads a JSONL trace of tool calls, applies a configured round-trip tax, and scans listed paths for crude secret shapes. It then prints LOCAL_HOLD or SERVER_BURST so a wrapper script can fail closed without a human staring at the spinner. Operators should treat the numbers as inputs they measure on their own network, not as published product claims.
#!/usr/bin/env python3
"""hop_gate.py — proposed preflight, not a production DLP control."""
from __future__ import annotations
import argparse
import json
import re
import socket
import sys
from pathlib import Path
SECRET_SHAPES = (
re.compile(r"AKIA[0-9A-Z]{16}"),
re.compile(r"-----BEGIN (?:RSA )?PRIVATE KEY-----"),
re.compile(r"(?i)(api[_-]?key|secret|token)\s*[:=]\s*\S{12,}"),
)
def load_trace(path: Path) -> list[dict]:
rows = []
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line:
rows.append(json.loads(line))
return rows
def hop_tax_ms(rows: list[dict], rtt_ms: float) -> tuple[float, float, int]:
local = sum(float(r.get("local_ms", 0)) for r in rows)
hops = len(rows)
remote = local + hops * rtt_ms
return local, remote, hops
def secret_hits(roots: list[Path]) -> list[str]:
hits: list[str] = []
skip = {".git", "node_modules", "dist", ".venv"}
for root in roots:
candidates = [root] if root.is_file() else [
p for p in root.rglob("*")
if p.is_file() and p.stat().st_size < 1_000_000
]
for path in candidates:
if any(part in skip for part in path.parts):
continue
try:
text = path.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
if any(rx.search(text) for rx in SECRET_SHAPES):
hits.append(str(path))
return hits
def link_is_up(host: str, port: int, timeout: float) -> bool:
try:
with socket.create_connection((host, port), timeout=timeout):
return True
except OSError:
return False
def decide(hops: int, secrets: list[str], online: bool, chatty_at: int) -> str:
# Gray zone (4..chatty_at-1) stays local so export is never the default.
if secrets or not online or hops >= chatty_at:
return "LOCAL_HOLD"
if hops <= 3:
return "SERVER_BURST"
return "LOCAL_HOLD"
def main() -> int:
p = argparse.ArgumentParser(description="Proposed hop gate for local-first agents")
p.add_argument("--trace", type=Path, required=True)
p.add_argument("--rtt-ms", type=float, default=180.0)
p.add_argument("--scan", type=Path, nargs="*", default=[])
p.add_argument("--probe-host", default="example.com")
p.add_argument("--probe-port", type=int, default=443)
p.add_argument("--chatty-at", type=int, default=8)
args = p.parse_args()
rows = load_trace(args.trace)
local_ms, remote_ms, hops = hop_tax_ms(rows, args.rtt_ms)
secrets = secret_hits(args.scan) if args.scan else []
online = link_is_up(args.probe_host, args.probe_port, timeout=1.5)
decision = decide(hops, secrets, online, args.chatty_at)
json.dump(
{
"hops": hops,
"local_ms": round(local_ms, 1),
"remote_ms_estimate": round(remote_ms, 1),
"rtt_ms": args.rtt_ms,
"secret_hits": secrets,
"online": online,
"decision": decision,
},
sys.stdout,
indent=2,
)
sys.stdout.write("\n")
return 0 if decision == "SERVER_BURST" else 2
if __name__ == "__main__":
raise SystemExit(main())
A tiny trace file makes the tax visible without pretending to be a published benchmark from a vendor lab. Save it as session.jsonl beside the script and keep payload bodies out of the log.
{"tool": "rg", "local_ms": 42}
{"tool": "rg", "local_ms": 38}
{"tool": "rg", "local_ms": 51}
{"tool": "pytest", "local_ms": 860}
{"tool": "rg", "local_ms": 40}
{"tool": "rg", "local_ms": 33}
{"tool": "rg", "local_ms": 47}
{"tool": "ruff", "local_ms": 120}
{"tool": "rg", "local_ms": 36}
{"tool": "pytest", "local_ms": 910}
python3 hop_gate.py --trace session.jsonl --rtt-ms 180 --scan . --chatty-at 8; echo exit:$?
Ten serial tools at one hundred eighty milliseconds of round-trip tax add eighteen hundred milliseconds before remote compute starts. The local column stays near two seconds of tool time, while the remote estimate crosses three and a half seconds on hops alone. That gap is why chatty loops stay on the laptop even when a free server would win a single long compile. A wrapper can treat a non-zero exit as LOCAL_HOLD and refuse to start a remote session.
Sessions with four to seven hops stay local in this proposed gate so the gray zone does not silently export a working tree. The printed remote_ms_estimate remains a teaching number: it shows the tax, and it does not override a secret hit or an offline probe. Operators should replace the default one hundred eighty milliseconds with a ping they took on the same link the laptop will use.
A six-step workflow
The sequence below is a concrete workflow, not a claim that any particular team has already shipped it to production.
- Record one representative session as JSONL with each tool name and locally measured duration, and never log file contents or environment values.
- Measure round-trip time to the candidate remote endpoint from the same network the laptop will actually use during the job. Office Ethernet, home Wi-Fi, and a phone tether disagree enough to change the hop gate outcome.
- Run the hop gate against the working tree paths the agent would read, including dotenv files, fixture dumps, and leftover snapshots.
- Treat any secret-shaped hit as a hard
LOCAL_HOLD, even if the remote estimate looks faster on paper than the local column. Regex patterns are a tripwire for obvious material and are not a clearance to export a repository. - If the gate returns
SERVER_BURST, send only a secret-stripped scaffold and keep the dirty tree on the laptop until a reviewable patch returns. Interactive follow-up turns remain local after the batch lands so the hop tax does not return through chat. - Re-run the same batch locally as a control when time allows, and keep both traces beside the decision for the next session.
Step five is where a free server earns its keep on coarse work rather than on chatty search loops. Coarse jobs with few hops can overlap compile and test on spare remote capacity while the laptop stays responsive for review. Batch reasoning belongs off-laptop only when the scaffold that leaves the machine has already failed closed on secrets and hop count.
Limitations, and who should skip this gate
The script estimates serial round-trip tax and does not model queueing, thermal throttling, or disk cache effects on either side. It will over-punish a remote job that batches tool calls, and it will under-punish a local model that swaps on a small laptop. The secret patterns miss cloud tokens that do not look like AWS keys, and they false-positive on docs that quote a PEM header. Missing traces, skipped scans, and ambiguous probes must fail closed, which means the working set stays on the laptop.
This approach is the wrong control for regulated repositories that need real DLP, allowlists, and durable audit logs. It is also the wrong control for air-gapped shops that already forbid egress, because the interesting placement decision never arises. Teams that pair through an agent on every keystroke should stay local regardless of a SERVER_BURST hint from the script. Free model access and a free server option are availability claims, not promises of capacity, permanence, or a particular model catalog. Operators should verify current limits in the product's own documentation before scheduling overnight batches around those options.
Hop count is a practical proxy for whether an agent session is a conversation with the tree or a batch thrown over the wall. Conversations stay local because each reply pays the network, and secrets in the working copy never get a chance to ride along. Batches may leave only after latency, residency, and offline vetoes all pass, which is a narrower win than a slogan about remote speed.
Top comments (0)