A remote hop without a local checkpoint is a dropped loop. The agent should write state to disk first. Only then may a remote path see a sanitized slice.
The pattern is older than current language models. Databases flush a write-ahead log before replicas. An agent loop needs that same habit on every exit.
Industry talk still paints agents as deep planners. Many production loops are tool calls wrapped around hope. Hope is not a recovery plan for hops.
This article stays local-first on every hop. It uses a checkpoint, a probe, and a short lease. The remote side is optional overflow, not the source of truth.
Cafe networks punish chat-shaped agents without local state. The first tool call stays local and cheap. The next call wants a model the laptop does not host.
The process posts a prompt and dies mid-flight. The outbound prompt vanishes with the broken socket. Partial tool output vanishes with the dying process.
Secrets in the buffer may already sit on the wire. That failure is not an intelligence gap at all. It is a storage and routing gap instead.
Treat the loop like a small database, not a chat window. Keep three records beside the repository root. The checkpoint stores the goal, the step index, and last output.
The lease stores a nonce and an expiry for one in-flight hop. The queue stores sanitized work that has not earned a hop. Think of the lease as a library card at the desk.
The book may leave while the card remains. The desk waits for a return or a timeout. Crash recovery should read those files before any new plan.
Replay starts from checkpoint.json, never from chat history. Chat history is a UI buffer and it lies after a kill. The checkpoint is the only record the next process should trust.
The JavaScript below is a proposal, not a shipped runtime. Comments mark example constants that you must replace. Do not treat the timeouts as measured SLAs.
// proposal: write-ahead checkpoint before any remote hop
import { createHash, randomBytes } from "node:crypto";
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { join } from "node:path";
const DIR = join(process.cwd(), ".loop-state");
const EXAMPLE_PROBE_MS = 150; // example threshold, not a measured SLA
const EXAMPLE_LEASE_MS = 20_000; // example lease budget, replace in config
function ensureDir() {
if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true });
}
function atomicWrite(name, obj) {
ensureDir();
const tmp = join(DIR, `${name}.tmp`);
const dest = join(DIR, name);
writeFileSync(tmp, JSON.stringify(obj, null, 2));
writeFileSync(dest, readFileSync(tmp));
}
function readJson(name, fallback) {
const p = join(DIR, name);
if (!existsSync(p)) return fallback;
return JSON.parse(readFileSync(p, "utf8"));
}
const SECRET_RE =
/\b(api[_-]?key|secret|token|password)\b\s*[:=]\s*\S+|bearer\s+[a-z0-9\-._~+/]+=*/gi;
function redact(text) {
return String(text).replace(SECRET_RE, "[REDACTED]");
}
function fingerprint(text) {
return createHash("sha256").update(text).digest("hex").slice(0, 12);
}
export function checkpoint(step) {
const safe = {
goal: redact(step.goal),
index: step.index,
lastOutput: redact(step.lastOutput || ""),
localOnly: Boolean(step.localOnly),
forbidRemoteSecrets: Boolean(step.forbidRemoteSecrets),
needRemoteCapacity: Boolean(step.needRemoteCapacity),
writtenAt: Date.now(),
};
atomicWrite("checkpoint.json", safe);
return safe;
}
export function queueHop(payload) {
const body = redact(payload);
const item = {
id: randomBytes(8).toString("hex"),
body,
bytes: Buffer.byteLength(body),
fp: fingerprint(body),
enqueuedAt: Date.now(),
};
const q = readJson("queue.json", []);
q.push(item);
atomicWrite("queue.json", q);
return item;
}
Redaction stays blunt on purpose in this sketch. A regex will miss odd encodings and binary blobs. A blunt local filter still beats a raw paste into a remote form.
Fingerprint the redacted body before the hop. The hash is not a security proof by itself. It only blocks a second lease for the same payload after a crash.
Truncating SHA-256 to twelve hex characters is an example. Collisions are unlikely for a laptop queue. Raise the length if the queue lives longer than a day.
Atomic writes use a temp file then a replace. Node's writeFileSync on the dest is a simple stand-in. On POSIX you would rename the temp file over the dest.
Do not trust last week's cafe as health. Probe the host you intend to use. A short TCP connect with a hard timeout suffices here.
import net from "node:net";
export function probeHost(host, port, timeoutMs = EXAMPLE_PROBE_MS) {
return new Promise((resolve) => {
const started = Date.now();
const socket = net.connect({ host, port });
const timer = setTimeout(() => {
socket.destroy();
resolve({ ok: false, rttMs: Date.now() - started, reason: "timeout" });
}, timeoutMs);
socket.on("connect", () => {
clearTimeout(timer);
const rttMs = Date.now() - started;
socket.end();
resolve({ ok: true, rttMs, reason: "connect" });
});
socket.on("error", (err) => {
clearTimeout(timer);
resolve({
ok: false,
rttMs: Date.now() - started,
reason: err.code || "error",
});
});
});
}
The 150 millisecond figure is an example threshold. A transoceanic path will fail that probe every time. That failure is a signal to stay on disk.
Offline is not a special branch in this design. Offline is simply a probe that returns false. The queue keeps the sanitized payload until the wire earns another try.
Drain the queue in enqueue order after a green probe. Do not sort by model size or prompt drama. Fair order keeps fingerprints easy to reason about.
Once the probe passes, take a lease before bytes leave. A crash then leaves an expiry and a checkpoint, not a mystery. The next boot can replay the local step.
The next boot must not open a second remote call. Compare fingerprints against the leftover lease first. Wait out the hop if the lease is still alive.
export function takeLease(item, now = Date.now()) {
const lease = {
id: item.id,
fp: item.fp,
nonce: randomBytes(8).toString("hex"),
until: now + EXAMPLE_LEASE_MS,
};
atomicWrite("lease.json", lease);
return lease;
}
export function leaseAlive(lease, now = Date.now()) {
if (!lease || !lease.until) return false;
return now < lease.until;
}
export function decide(step, probe, lease, now = Date.now()) {
if (step.localOnly) return "local";
if (step.forbidRemoteSecrets && /\[REDACTED\]/.test(step.goal)) {
return "local";
}
if (!probe.ok) return "queue";
if (leaseAlive(lease, now)) return "wait";
if (step.needRemoteCapacity) return "remote";
return "local";
}
Read decide() as a gate rather than a score. Local work wins when the step is marked localOnly. Local work also wins when redaction still flags secrets and policy forbids travel.
The queue wins when the probe fails. Wait wins when a lease still owns the hop. Remote work wins only when local capacity is missing and the lease is free.
NeedRemoteCapacity is a boolean you set in the step. Do not hide it inside a magic latency number. A missing local model is a capacity miss.
A thermal throttle you can observe is a capacity miss. A vague belief that a cloud model is wiser is not. Keep that belief out of the gate.
A free remote server is still a hop. It wins inside a narrow window only. The laptop cannot host the model class the step needs.
The checkpoint must already exist on local disk. The payload must be redacted before the lease. The probe must be green and the lease must be free.
That window is real for small teams without a desk GPU. Shared overflow should not block on one laptop fan. A free server can hold overflow without a chat-shaped client.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode provides free model access and a free server option. Those two availability facts fit the overflow window above.
They do not replace the checkpoint, the probe, or the lease. Keep the vendor host in adapter config, not in the gate. The core protocol should still boot with the network unplugged.
export async function overflowHop(item, adapter) {
const lease = takeLease(item);
try {
const result = await adapter.complete(item.body);
atomicWrite("lease.json", { id: null, fp: null, until: 0 });
checkpoint({
goal: item.body,
index: "overflow",
lastOutput: redact(result.text || ""),
localOnly: false,
});
return { ok: true, lease, result };
} catch (err) {
return { ok: false, lease, error: String(err) };
}
}
The adapter is an injected function with one job. If the free server disappears, the checkpoint still boots. The queue still holds the redacted work.
Expired leases are not automatic retries. Read the checkpoint, then decide again with a fresh probe. A blind retry is how duplicate remote thoughts get born.
Tests should prove the gate, not a vendor. Node's assert module is enough for this sketch. Fail local redaction before you debug any remote adapter.
import assert from "node:assert/strict";
import {
checkpoint,
queueHop,
decide,
takeLease,
leaseAlive,
} from "./loop_lease.js";
const step = checkpoint({
goal: "summarize README; token=ghp_example",
index: 0,
lastOutput: "",
localOnly: false,
forbidRemoteSecrets: true,
});
assert.match(step.goal, /\[REDACTED\]/);
assert.equal(decide(step, { ok: true, rttMs: 40 }, null), "local");
const item = queueHop("explain the gate; api_key=abcd");
assert.equal(item.body.includes("abcd"), false);
assert.equal(decide(step, { ok: false, rttMs: 150 }, null), "queue");
const remoteStep = { ...step, forbidRemoteSecrets: false, needRemoteCapacity: true };
assert.equal(decide(remoteStep, { ok: true, rttMs: 40 }, null), "remote");
const lease = takeLease(item, 1_000);
assert.equal(leaseAlive(lease, 1_500), true);
assert.equal(leaseAlive(lease, 30_000), false);
assert.equal(
decide(remoteStep, { ok: true, rttMs: 40 }, lease, 1_500),
"wait",
);
Run the file with node --test loop_lease.test.js after you split modules. If redaction fails, stop the loop at once. Do not "just try" the hop to see what happens.
This design will not pass an air-gapped audit. If bytes must never leave the machine, drop the adapter. Set localOnly on every step and skip the lease.
A regex is not a complete secret scanner. Rotate any token that has lived in a prompt buffer. The fingerprint only prevents double hops after a crash.
This article claims no region, quota, or uptime for overflow. Treat that remote path as best-effort overflow. Do not park billing, medical, or identity flows on it.
Do not measure success by hop count. Measure success by crashes that reload the same checkpoint. Teams with a real inference SLA should buy that SLA.
The lease pattern still applies when the vendor changes. Cafe Wi-Fi will still lie about health. Captive portals accept TCP and then hijack HTTP.
If the adapter speaks HTTP, check the body, not only connect. Put that content check inside the adapter only. Keep the decide() gate boring and local.
Skip this approach if a single local model already covers the work. Skip it if compliance forbids every remote complete call. Skip it if the goal was a chat UI rather than a loop.
Use it if agents already write files and stall on flaky links. Use it if state belongs on disk even when overflow is free. Use it if a remote path is overflow, not identity.
The loop remains yours when the wire goes quiet. Checkpoint first, then probe, then take the lease. Hop last, and only with a redacted slice.
If checkpoints already sit on disk, overflow stays a config choice. Point the adapter at MonkeyCode's free server after a green probe. Keep the adapter injectable so the gate never leaves the box.
Top comments (1)
WAL as the agent-state frame is good. The gap: side effect succeeded but reply lost, so a naive retry double-applies. I pair every checkpoint with an idempotency key the remote must honor.