
Your AI Agent Is a Job, Not an HTTP Request
GPT-6 Astra reportedly spends about 40 minutes on a single OSWorld 2.0 task. That changes the architecture of AI agents: durable checkpoints, named steps, graceful shutdowns, and idempotent side effects are no longer optional.
OpenAI's GPT-6 Astra launch has been framed around benchmark scores. The more important number is the duration.
On the offline subset of OSWorld 2.0, OpenAI reports Astra spending roughly 40 minutes per task. The model is supposed to control a computer through long, multi-step workflows: filling out spreadsheets, building websites, and carrying work through to completion.
The scores are vendor-reported and were not independently verified at the time of writing. OpenAI reported 98.6% on ARC-AGI-3, 97.6% on FrontierMath Tier 4 v2, 96% on GPQA Diamond, 95.9% on BenchCAD, 74.1% on DeepSWE v1.1, and 72.6% on OSWorld 2.0's offline subset.
The benchmark debate can wait. Forty minutes is an architectural boundary.
A forty-minute model run is not an API request that happens to take a while. It is a job. Jobs need durable state, restart behavior, cancellation semantics, and accounting that survives the process running them.
If your agent currently lives inside one HTTP request, one websocket, one pod, or one browser tab, it probably cannot survive the workload these models are moving toward.
The failure mode is boring—and expensive
Imagine an agent starts at 14:02. It reads a ticket, opens a repository, edits four files, runs the test suite, reads the failures, and edits two more files.
At 14:33, someone merges to main. The deployment rolls your pods. The process disappears in the middle of a tool call.
At 14:34, the user presses retry.
The agent reads the ticket again. It opens the repository again. It edits the same four files again.
Thirty-one minutes of model work are gone. Nothing necessarily appears in Sentry. The process may have exited cleanly from the platform's perspective. Kubernetes did what it was told to do.
Short agent runs have hidden this problem. A 20-second run that dies and retries is annoying, but often invisible. A run that consumes 40 minutes of model time before failing is a different class of operational problem.
Artificial Analysis lists Astra at $10 per million input tokens and $50 per million output tokens, with a 1M-token context window, as of the source article's publication. To make the cost concrete, a run that has generated 200,000 output tokens before dying represents $10 of output-token spend that may be paid again on retry. That is illustrative, not a prediction of what your workload will cost.
The source article also notes a favorable detail: Astra used 16M output tokens across its index run against a 62M median, making it unusually concise for its tier. Concise helps. It does not make failed work free.
The number you need is not the model's benchmark score. It is your own cost per abandoned run, multiplied by your retry rate.
What kills a long-running agent?
The model is often the least interesting failure point.
A deployment can send SIGTERM, wait through a 30-second terminationGracePeriodSeconds, and remove the process while the agent is still deep inside a task.
An HTTP request can hit a timeout before the work is complete. An ALB's idle timeout starts at 60 seconds. nginx's proxy_read_timeout also starts at 60 seconds. Cloudflare's free tier gives you 100 seconds before a 524. These settings can be changed, but the default infrastructure around your application is not designed around forty-minute requests.
The user's browser is another fragile boundary. They close the tab. Their laptop sleeps. The connection drops. If the job is tied to a websocket, the job may disappear with the connection.
Hosted compute has limits too. Lambda stops at 15 minutes. Vercel functions stop at 800 seconds on Pro, with a 30-minute extended-duration beta. Cloud Run request mode defaults to 5 minutes and can be raised to 60 minutes, but Google's documentation still recommends idempotent handlers or handlers that can resume from where they stopped for work lasting beyond 15 minutes.
And then there is memory. Long runs accumulate context. A 1M-token context window gives an agent room to carry a lot of history, but it does not guarantee that your process, framework, or surrounding tools will handle that history cheaply.
The shared shape is simple:
Your process boundary is shorter than your task boundary.
Making the process live longer is not a complete fix. Deploys, node failures, timeouts, disconnected clients, and memory pressure still exist.
The task has to survive the process.
Make the step the unit of work
The durable pattern is not complicated:
- Break the task into named steps.
- Persist each completed step immediately.
- On restart, load the saved state.
- Skip steps that are already complete.
- Continue from the first unfinished step.
The important detail is the identity of a step. Use a stable name, not an array index.
Index-based resume works until you insert a new step into the plan. Then the old step 3 may refer to a completely different operation. A run resumes, sees that index 3 is complete, and skips work it never actually performed.
A name-based state model avoids that problem:
export type Usage = { input: number; output: number };
export type StepResult = {
name: string;
output: unknown;
usage: Usage;
finishedAt: string;
};
export type RunState = {
runId: string;
task: string;
completed: StepResult[];
totals: Usage;
};
export function emptyRun(runId: string, task: string): RunState {
return {
runId,
task,
completed: [],
totals: { input: 0, output: 0 },
};
}completed is an append-only record of what happened, not a cursor showing where the loop stopped. That distinction matters when the plan changes between attempts.
totals matters for the same reason. If token accounting lives only in a variable inside the process, a restart resets the numbers. Your budget ceiling then forgets what the previous process already spent.
Checkpoint after every completed step
A filesystem-backed checkpoint is enough to make the durability boundary obvious while developing locally. The key detail is atomic replacement.
A plain writeFile can leave a truncated JSON file if the process dies partway through the write. Write to a temporary file first, then rename it into place:
import {
mkdir,
readFile,
rename,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import type { RunState } from "./state.js";
const DIR = process.env.AGENT_STATE_DIR ?? ".agent-runs";
const pathFor = (runId: string) =>
join(DIR, `${runId}.json`);
export async function save(state: RunState): Promise<void> {
await mkdir(DIR, { recursive: true });
const target = pathFor(state.runId);
const tmp = `${target}.${process.pid}.tmp`;
await writeFile(tmp, JSON.stringify(state), "utf8");
await rename(tmp, target);
}
export async function load(
runId: string,
): Promise<RunState | null> {
try {
const raw = await readFile(pathFor(runId), "utf8");
return JSON.parse(raw) as RunState;
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === "ENOENT") return null;
throw err;
}
}Missing state means this is the first attempt. Other errors should remain errors. If you swallow every exception, a permissions problem or corrupt state directory looks like a fresh run, and the agent silently repeats work you already paid for.
In production, the same interface can sit on top of Postgres, Redis, or DynamoDB. The storage technology is less important than the guarantee: save should return only after the checkpoint is durable.
Do not put this state on a pod's ephemeral disk in Kubernetes. That defeats the whole purpose. The checkpoint must outlive the process and the machine running it.
Resume by name, not position
A step can be any asynchronous operation. The runner does not need to know whether it calls a model, runs a test suite, or invokes an internal API.
export type Ctx = {
runId: string;
task: string;
prior: Record<string, unknown>;
};
export type Step = {
name: string;
run: (ctx: Ctx) => Promise<{
output: unknown;
usage: Usage;
}>;
};The runner reconstructs two things from the saved state: a set of completed step names and a map of their outputs.
let draining = false;
const drain = () => {
draining = true;
};
process.on("SIGTERM", drain);
process.on("SIGINT", drain);
export async function runPlan(
runId: string,
task: string,
plan: Step[],
): Promise<RunState> {
const state = (await load(runId)) ?? emptyRun(runId, task);
const done = new Set(state.completed.map((step) => step.name));
const prior: Record<string, unknown> = {};
for (const step of state.completed) {
prior[step.name] = step.output;
}
for (const step of plan) {
if (done.has(step.name)) continue;
if (draining) return state;
const { output, usage } = await step.run({
runId,
task,
prior,
});
prior[step.name] = output;
state.completed.push({
name: step.name,
output,
usage,
finishedAt: new Date().toISOString(),
});
state.totals.input += usage.input;
state.totals.output += usage.output;
await save(state);
}
return state;
}Three lines carry most of the design.
if (done.has(step.name)) continue prevents already-paid work from running again.
if (draining) return state lets the process stop between steps. The runner finishes the step already in progress, checkpoints it, and refuses to start another one. The next process resumes the run.
await save(state) immediately after the completed step is the durability boundary. Move it outside the loop and you have built a system that checkpoints only successful runs—the exact runs that did not need recovery.
Set terminationGracePeriodSeconds comfortably longer than your slowest individual step. Keep steps short enough to drain safely. A step that takes 20 minutes is difficult to shut down gracefully, even if the overall job is checkpointed correctly.
Checkpointing is not rollback
There is a dangerous gap between performing an external side effect and saving the checkpoint.
Suppose step three calls a payments API. The API succeeds. Then the process dies before save commits. On restart, the state says step three never happened, so the runner calls the payments API again.
Now the customer may be charged twice.
Checkpointing remembers what your process knows. It cannot undo what the outside world already observed.
Every effectful step needs an idempotency key derived from the run and the step:
async function chargeCustomer(
ctx: { runId: string },
stepName: string,
amountCents: number,
) {
return stripe.paymentIntents.create(
{
amount: amountCents,
currency: "eur",
},
{
idempotencyKey: `${ctx.runId}:${stepName}`,
},
);
}If the step is replayed, the downstream service can return the original result instead of creating a second effect. Payment and booking APIs commonly support this pattern, and an internal API can support it too.
The narrower rule is even more useful: put at most one external side effect in a step. A step that performs three writes and dies after the second is much harder to replay safely.
There is another issue that gets more serious as runs get longer: prompt injection. The source article cites an OpenAI system card with external evaluations reporting an 8.5% prompt-injection attack success rate for Astra, compared with 27.0% for its predecessor. Those are vendor-published figures, and they are not zero.
A resumed run faithfully reconstructs its prior state. That is good for reliability. It also means you should not confuse remembered instructions with trusted instructions. Checkpoint the state. Do not checkpoint your trust in model-generated content or external data.
The practical rule
If a run can outlive an HTTP request, treat it as a job.
Give it a run ID. Decompose it into named steps. Checkpoint after every step. Resume by name. Track usage in durable state. Put an idempotency key on anything that touches the outside world.
This is not specific to Astra, OpenAI, or 2026. It is ordinary job-processing architecture applied to model-driven work.
The reason it is becoming urgent now is duration. When an agent takes seconds, restarting feels harmless. When it takes tens of minutes, restarting is a direct cost, a poor user experience, and potentially a duplicate side effect.
Go look at your longest agent run from last week. Ask what happens if you deploy during it. Ask what happens if the browser disconnects. Ask what happens if the process dies immediately after a tool call succeeds.
If the answer is that the agent starts over, the next feature is not a better prompt. It is durable state.
Related posts
- Automation
CloakBrowser: I tested it against 5 bot detectors — here's what happened
CloakBrowser claims to be a stealth Chromium that passes every bot detection test. I installed it, ran it against reCAPTCHA v3, Cloudflare Turnstile, and FingerprintJS to see if the hype is real.
May 19, 2026 · 8 min - Automation
Automating web3 workflows at scale — a sanitized case study
How I built custom tooling to manage hundreds of wallets, automate on-chain transactions, and run social bots across multiple protocols.
May 18, 2026 · 10 min