cd /news/ai-agents/letter-to-friday-me-budget-isolate-t… · home topics ai-agents article
[ARTICLE · art-128511] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Letter to Friday-Me: Budget, Isolate, Then Stamp the Receipt

A developer published a reconstructed failure log describing how an uncapped coding agent loop on a free remote server consumed an entire working day, with no receipt recording the runner, files, or stop condition. The writeup argues that free model access and free server time are not infinite budgets, and proposes a local control plane enforcing three gates — budget, isolation, and a session receipt — before any prompt is sent. The workflow is presented as product-agnostic, runnable on any SSH box and model endpoint, with sample Node.js code that hashes state and refuses to proceed on a dirty working tree.

by read7 min views1 publishedSep 13, 2026

The following letter is a reconstructed failure log.

It is not a claim about any employer.

Friday at 16:40, the coding agent was still looping.

No receipt named the runner, files, or stop condition.

Friday-me,

You treated a free remote loop as unbounded compute.

That single assumption consumed an entire working day.

Three process mistakes did the real damage here.

None of them were about model quality.

A refactor request looked small at noon.

You pointed an agent at a dirty working tree.

You let it retry on a remote box with no cap.

By late afternoon the tree was noisy.

Two patches conflicted, and one secret file entered context.

You could not reconstruct the successful step.

This letter is the workflow you lacked.

It is a local control plane for free remote loops.

Free model access does not mean an infinite loop.

Free server time does not mean an infinite retry budget.

Without a cap, failure becomes a quiet background process.

You watched tokens burn through duplicate tool calls.

The same failing test command ran without a recorded cap.

No file recorded the step limit or the continue reason.

A loop without a stop rule is not an agent.

It is a cron job you forgot to kill.

The remote workspace inherited your entire laptop state.

node_modules, .env, and a half-applied patch came along.

The model then fixed symptoms that were only local dirt.

Context windows then filled with unrelated local diffs.

The runner executed scripts against stale local dependencies.

You debugged the model instead of the dirty tree.

Isolation is not a style choice here.

It is the difference between a session and a mess.

When the loop died, evidence died with it.

You had chat scrollback and a dirty git status.

You did not have a hash, a file list, or a budget remainder.

Monday-you cannot replay what Friday-you actually ran.

Support cannot tell which runner produced the patch.

You spend the next day reconstructing a session that never closed.

A session receipt is not extra process theater.

It is the smallest artifact that makes the day replayable.

Keep exploratory loops off the laptop when the tree is messy.

A free remote server plus free model access can host that drill.

MonkeyCode currently offers free model access and a free server option.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The workflow below does not depend on that product.

Any SSH box and any model endpoint can run the same receipt.

Follow these steps before the first prompt.

Do not reorder them under time pressure.

max_steps or max_seconds hits zero. If any step fails, delete the workspace.

Do not just retry inside the dirty tree.

The script below is an example control plane.

Treat it as unexecuted sample code, not a benchmark.

It enforces three gates: budget, isolation, and receipt.

It never sends secrets and it never applies a patch.

#!/usr/bin/env node
// Example only. Review before use. Not a hosted service client.

import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import {
  existsSync,
  readFileSync,
  writeFileSync,
} from "node:fs";
import path from "node:path";

const ROOT = process.cwd();
const RECEIPT = path.join(ROOT, ".session-receipt.json");
const FORBIDDEN = new Set([".env", ".env.local", "id_rsa", "id_ed25519"]);

function git(args) {
  return execFileSync("git", args, { encoding: "utf8" }).trim();
}

function sha256(text) {
  return createHash("sha256").update(text).digest("hex");
}

function load() {
  if (!existsSync(RECEIPT)) {
    throw new Error("no open receipt; run init first");
  }
  return JSON.parse(readFileSync(RECEIPT, "utf8"));
}

function save(doc) {
  writeFileSync(RECEIPT, JSON.stringify(doc, null, 2) + "\n");
}

function assertCleanAllowlist(allow) {
  const dirty = git(["status", "--porcelain"]);
  if (dirty) {
    throw new Error("worktree is dirty; isolate first");
  }
  for (const rel of allow) {
    const base = path.basename(rel);
    if (FORBIDDEN.has(base)) {
      throw new Error(`forbidden path in allowlist: ${rel}`);
    }
  }
}

function init() {
  const maxSteps = Number(process.env.MAX_STEPS || 8);
  const maxSeconds = Number(process.env.MAX_SECONDS || 900);
  const allow = (process.env.ALLOW || "src,tests")
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);

  if (maxSteps < 1 || maxSteps > 32) {
    throw new Error("MAX_STEPS must be between 1 and 32");
  }

  assertCleanAllowlist(allow);

  const doc = {
    status: "open",
    opened_at: new Date().toISOString(),
    git_sha: git(["rev-parse", "HEAD"]),
    branch: git(["rev-parse", "--abbrev-ref", "HEAD"]),
    allowlist: allow,
    budget: {
      max_steps: maxSteps,
      max_seconds: maxSeconds,
      steps_used: 0,
    },
    runner: {
      host: process.env.RUNNER_HOST || "unspecified",
      kind: process.env.RUNNER_KIND || "free-remote",
    },
    model: {
      endpoint: process.env.MODEL_ENDPOINT || "unspecified",
    },
    artifact: null,
  };

  if (existsSync(RECEIPT)) {
    throw new Error("receipt already exists; close or delete it");
  }
  save(doc);
  console.log("receipt opened:", RECEIPT);
}

function tick() {
  const doc = load();
  if (doc.status !== "open") {
    throw new Error("receipt is not open");
  }
  const started = Date.parse(doc.opened_at);
  const elapsed = (Date.now() - started) / 1000;
  doc.budget.steps_used += 1;

  if (doc.budget.steps_used > doc.budget.max_steps) {
    throw new Error("step budget exhausted");
  }
  if (elapsed > doc.budget.max_seconds) {
    throw new Error("time budget exhausted");
  }
  save(doc);
  console.log(
    `step ${doc.budget.steps_used}/${doc.budget.max_steps}; ${Math.floor(elapsed)}s elapsed`,
  );
}

function close() {
  const doc = load();
  const diffPath = process.env.DIFF_PATH || "agent.patch";
  if (!existsSync(diffPath)) {
    throw new Error(`missing diff: ${diffPath}`);
  }
  const diff = readFileSync(diffPath, "utf8");
  if (!diff.startsWith("diff --git ") && !diff.startsWith("--- ")) {
    throw new Error("artifact is not a unified diff");
  }
  if (diff.length > 200_000) {
    throw new Error("diff exceeds 200k characters; split the task");
  }

  doc.status = "closed";
  doc.closed_at = new Date().toISOString();
  doc.artifact = {
    path: diffPath,
    sha256: sha256(diff),
    bytes: Buffer.byteLength(diff),
  };
  save(doc);
  console.log("receipt closed:", doc.artifact.sha256);
}

const cmd = process.argv[2];
 if (cmd === "init") init();
else if (cmd === "tick") tick();
else if (cmd === "close") close();
else {
  console.error("usage: receipt.mjs <init|tick|close>");
  process.exit(2);
}

Use a detached worktree for every agent session.

Do not run this script in a dirty repo.

git fetch --quiet
SHA=$(git rev-parse HEAD)
git worktree add --detach ../agent-scratch "$SHA"
cd ../agent-scratch

export MAX_STEPS=8
export MAX_SECONDS=900
export ALLOW=src,tests
export RUNNER_HOST=free-server.example
export RUNNER_KIND=free-remote
export MODEL_ENDPOINT=unspecified

node ../main/receipt.mjs init

node ../main/receipt.mjs tick

export DIFF_PATH=agent.patch
node ../main/receipt.mjs close

Wrap every remote tool invocation with a tick call.

If tick exits non-zero, kill the loop.

MAX_STEPS=8 is a local policy, not a vendor quota.

It forces a split when the task is still vague.

Raise it only after the allowlist shrinks.

MAX_SECONDS=900 bounds retries you will not watch.

Network blips should not become an overnight process.

Pair it with a shell timeout around SSH.

The 200k diff cap blocks silent tree dumps.

A useful patch names files and stays reviewable.

If the artifact is larger, the task was too wide.

Condition Local laptop Isolated free server
Secrets in tree Stop. Redact first. Do not copy.
Dirty git status Isolate or revert. Fresh worktree only.
Exploratory refactor Optional. Preferred with a budget.
Prod deploy Human-owned. Never.
Unknown model endpoint Do not send code. Do not send code.
Need replay on Monday Receipt required. Receipt required.

Read the decision table before you prompt.

If two rows conflict, choose the stricter one.

Keep the receipt document small, complete, and boring.

These fields are the minimum useful set.

git_sha of the isolated tree.allowlist of paths copied to the runner.budget.max_steps and budget.max_seconds. runner.host and runner.kind. model.endpoint without secrets.artifact.sha256 of the unified diff. Do not store prompts that still contain credentials.

Do not store raw env values for later debug.

Name the endpoint as a hostname only.

If you cannot name it, do not open the receipt.

This receipt is a local file, not a signed audit log.

Anyone with disk access can edit it after the fact.

A free server is not a tenancy boundary.

It is not a SOC2 control or an availability SLA.

The example script does not apply any patches.

It does not sandbox syscalls or prove runner honesty.

The budget counters remain local to the process.

A crashed wrapper can skip the tick call.

Pair this with a wall-clock timeout on the SSH session.

timeout 15m ssh runner 'cd /work && ./run-agent.sh'

If you need cryptographic provenance, add detached signatures.

This article does not implement that layer.

Skip this workflow in the following cases.

These constraints are hard limits, not style notes.

Do not send regulated data to a free remote model.

Do not copy customer exports onto a free server.

Do not run this as your production CI.

Security teams that need attestation want a different stack.

Students on a shared laptop should still isolate paths.

If you cannot name the endpoint, do not start the loop.

Friday-me, the model was not the outage.

The missing stop rule was the outage.

The dirty tree and missing receipt were the outage.

Bound the loop, isolate the tree, and stamp the receipt.

Then you can spend Monday on the patch, not the archaeology.

MonkeyCode is one place to try a free server for this drill.

── more in #ai-agents 4 stories · sorted by recency
── more on @monkeycode 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/letter-to-friday-me-…] indexed:0 read:7min 2026-09-13 ·