cd /news/artificial-intelligence/openai-says-gpt-6-astra-runs-40-minu… · home topics artificial-intelligence article
[ARTICLE · art-120860] src=dev.to ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

OpenAI Says GPT-6 Astra Runs 40 Minutes on One Task. Your Agent Loop Probably Can't.

OpenAI's GPT-6 Astra, announced on September 3, 2026, reports a 72.6% score on the OSWorld 2.0 benchmark, with tasks taking roughly 40 minutes each, highlighting the shift from short agent runs to long-horizon tasks that require new infrastructure. The model also shows near-saturated scores on knowledge benchmarks (95.9%-98.6%) but a persistent gap on agentic tasks, and is priced at $10 per million input tokens and $50 per million output.

read16 min views1 publishedSep 3, 2026

Your agent starts a task at 14:02. It reads the ticket, opens the repo, edits four files, runs the test suite, reads the failures, edits two more files. At 14:33 someone merges to main and the deploy rolls your pods. The process disappears mid-tool-call.

At 14:34 the user hits retry. The agent reads the ticket. It opens the repo. It edits four files.

Thirty-one minutes of tokens, gone, and you are paying for the second attempt at the same work. Nothing crashed in a way you would see in Sentry. The pod exited 0. Kubernetes did what you told it to do.

This failure mode has been survivable for two years because runs were short. A 20-second agent run that dies gets retried and nobody notices. That is the part that just changed.

OpenAI announced GPT-6 Astra on 3 September 2026. The launch coverage led with percentages, and the percentages are high. But the number that should change your architecture is on the OSWorld 2.0 line, and it is not a percentage.

Here are the scores OpenAI reported at launch. All of these are vendor-reported and not independently verified at the time of writing:

Benchmark OpenAI-reported score
ARC-AGI-3 98.6%
FrontierMath Tier 4 v2 97.6%
GPQA Diamond 96%
BenchCAD 95.9%
DeepSWE v1.1 74.1%
OSWorld 2.0 (offline subset) 72.6%

On that OSWorld 2.0 subset, OpenAI reports the model spending roughly 40 minutes per task. OpenAI calls Astra a new high-water mark for autonomously controlling computer systems: filling out spreadsheets, building websites from scratch. It also says the model stays oriented better and carries multi-step workflows to the end. VentureBeat's launch writeup has the full set.

Forty minutes is longer than most HTTP timeouts. It is longer than a Lambda invocation can run. It is longer than the gap between two deploys on a busy afternoon. A model that works for forty minutes on one task is not a request. It is a job, and jobs need different plumbing than requests do.

Greg Brockman, OpenAI's co-founder and president, said "I think it's not unreasonable to feel that we are now in the AGI era" (quoted in VentureBeat's writeup). That is his framing, and it is an opinion about the field rather than a measured result. The forty minutes is the part you have to write code against either way.

Look at the table again as two groups.

The knowledge-shaped benchmarks sit between 95.9% and 98.6%. Those are near enough to saturated that the remaining points are mostly argument about the benchmark. The two agentic ones sit at 74.1% and 72.6%.

Reading those published numbers straight: something in the neighbourhood of one in four attempts on those benchmark tasks does not land. That is an inference from the reported percentages, not a measurement. The two benchmarks are not strictly comparable — different tasks, different distributions. And neither is a prediction about your workload. Your tasks are not their tasks.

But the direction is hard to argue with. The gap between "knows things" and "does things over a long horizon" is still about twenty points wide, and the failures on the doing side are expensive in a way the failures on the knowing side are not. A wrong GPQA answer costs you a few hundred tokens. A failed OSWorld-style run costs you forty minutes of tokens, and you find out at the end.

The published pricing makes that concrete. As of publication, Artificial Analysis lists Astra at $10 per million input tokens and $50 per million output, with a 1M-token context window. One detail there is genuinely good news for long runs: Astra used 16M output tokens across their index run against a 62M median, so it is unusually concise for its tier.

Concise still is not free. Illustratively, on those published prices: a run that has produced 200,000 output tokens before it dies is $10 you throw in the bin, and you pay it again on the retry. Multiply that by however many runs a day your agent does, and by whatever your own retry rate turns out to be. You will not know that number until you measure it on your own workload, and it is the number that decides whether any of this is worth an afternoon.

Almost none of the deaths are the model's fault.

The deploy. Rolling update, SIGTERM, terminationGracePeriodSeconds: 30

, process gone. Your agent was 31 minutes into a 40-minute task.

The socket. If the run lives inside an HTTP request, every hop between the browser and your process has an opinion about how long a request may take. An ALB's idle timeout starts at 60 seconds. So does nginx proxy_read_timeout.

The tab. The user closes it, or their laptop sleeps, or the wifi drops on the train. If your job is anchored to a websocket, the job dies with the connection.

The platform limit. Lambda stops at 15 minutes. Vercel functions stop at 800 seconds on Pro, 30 minutes on the extended-duration beta. Cloud Run request mode defaults to 5 minutes and can be raised to 60, so it clears the bar — and Google's own documentation tells you not to trust that ceiling anyway. Past 15 minutes it recommends making requests idempotent, or "designing request handlers in such a way that they can resume from the point where they left off." That second half is this post, written by the platform vendor.

The OOM kill. Long runs accumulate context. A 1M-token window is an invitation to accumulate a lot of it.

The common shape: your process boundary is shorter than your task. You cannot fix that by making the process live longer, because the deploy will get you anyway. You fix it by making the task survive the process.

The pattern that survives is boring. Decompose the task into named steps. Persist the result of each step the moment it completes. On start, load whatever is on disk and skip every step already recorded.

The important design choice is that the step name is the identity, not the array index. Index-based resume breaks the first time you insert a step into the plan and an in-flight run comes back to find step 3 is now something else.

Start with the state:

// state.ts
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 list, not a cursor. A cursor tells you where you stopped; the list tells you what you have, which is what the resume actually needs when the plan has changed underneath it.

totals

is there so the accounting survives the restart too. If you track cost in a variable inside the loop, the restart resets your spend to zero and your budget ceiling stops meaning anything.

Filesystem first, because it is stdlib and it makes the durability boundary visible. The only subtlety is that a plain writeFile

is not atomic: if the process dies partway through, you get a truncated JSON file, and now your resume path throws on every attempt. Write to a temp file, then rename

, which is atomic within a filesystem on POSIX.

// checkpoint.ts
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(s: RunState): Promise<void> {
  await mkdir(DIR, { recursive: true });
  const target = pathFor(s.runId);
  const tmp = `${target}.${process.pid}.tmp`;
  await writeFile(tmp, JSON.stringify(s), "utf8");
  await rename(tmp, target);
}

The read side has one branch worth naming. A missing file is not an error here, it is the first run — so ENOENT

returns null

and everything else rethrows. Swallow all errors instead and a permissions problem or a corrupt directory looks exactly like a fresh start, and the run silently redoes work it already paid for.

// checkpoint.ts, continued

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;
  }
}

In production this becomes a Postgres row, a Redis key, or a DynamoDB item. The interface is the same two functions. What must not change is that save

returns only after the write is durable — if you swap in a store that acknowledges before it persists, you have a checkpoint that is a suggestion.

Do not put the state file on the pod's ephemeral disk in a Kubernetes deployment. The whole point is surviving the pod.

A step takes the outputs of the steps before it and returns its own output plus its token usage.

// steps.ts
import type { Usage } from "./state.js";

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 is short, and every line of it is doing something. The first half rebuilds whatever the previous process left behind:

// run.ts
import { load, save } from "./checkpoint.js";
import { emptyRun, type RunState } from "./state.js";
import type { Step } from "./steps.js";

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((s) => s.name),
  );
  const prior: Record<string, unknown> = {};
  for (const s of state.completed) {
    prior[s.name] = s.output;
  }

  if (done.size > 0) {
    console.log(
      `resuming ${runId}: ${done.size}/${plan.length} done`,
    );
  }

That is the entire resume, reconstructed from one file: a set of step names already finished, and a map of their outputs for the steps that come next. No cursor, no replay of anything, no coordination with whatever process wrote it.

The second half is the loop:

// run.ts, continued — still inside runPlan

  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 the whole design.

if (done.has(step.name)) continue;

is the resume. Work you already paid for is never paid for twice.

if (draining) return state;

is the graceful shutdown. SIGTERM is what your orchestrator sends before it kills the pod; SIGINT is what Ctrl-C sends, and it is handled here so you can watch the drain happen on your own machine. On either one the runner finishes the step it is in, checkpoints it, and refuses to start another. The next process picks the run up. Set terminationGracePeriodSeconds

to comfortably longer than your slowest single step, and keep steps short enough that this is possible — a step that takes 20 minutes is a step you cannot drain.

await save(state)

sitting immediately after the push is the durability boundary. State in memory and state on disk are never more than one step apart. Move that call outside the loop and you have written a program that checkpoints only runs that did not need it.

Everything so far is model-agnostic, which is the point. The runner never learns what a step does. A step is a name and a function, so the model call goes in one helper and every step is a call to it. Two details in that helper matter. ask

returns usage alongside the text, because the token totals are part of what has to survive a restart. And the model id comes from the environment, because the id you can actually reach changes faster than this code will.

// plan.ts  — npm i openai
import OpenAI from "openai";
import type { Step } from "./steps.js";

// Reads OPENAI_API_KEY from the environment.
const client = new OpenAI();

// Set AGENT_MODEL to the id your provider exposes.
const MODEL = process.env.AGENT_MODEL;
if (!MODEL) throw new Error("set AGENT_MODEL");

async function ask(prompt: string) {
  const r = await client.responses.create({
    model: MODEL,
    input: prompt,
  });
  return {
    output: r.output_text,
    usage: {
      input: r.usage?.input_tokens ?? 0,
      output: r.usage?.output_tokens ?? 0,
    },
  };
}

The plan itself is then a list. Two steps are enough to show the mechanism, because the second one reads the first one's output out of prior

— which is the only coupling between steps that exists.

// plan.ts, continued

export const plan: Step[] = [
  {
    name: "decompose",
    run: ({ task }) =>
      ask(
        `Break this into 4 concrete subtasks. ` +
          `One per line, no prose.\n\n${task}`,
      ),
  },
  {
    name: "draft",
    run: ({ task, prior }) =>
      ask(
        `Task: ${task}\n` +
          `Subtasks:\n${prior.decompose}\n\n` +
          `Produce the deliverable.`,
      ),
  },
];

Add research

before the draft and review

after it the same way: a name, a function, a read from prior

. Nothing in the runner changes when you do, and a run that was in flight under the old plan resumes correctly under the new one, because it matches on names rather than positions.

The entry point wires it together and prints the accounting, so you can watch the totals carry across a restart:

// main.ts
import { plan } from "./plan.js";
import { runPlan } from "./run.js";

const runId = process.argv[2];
const task = process.argv[3] ?? "";

if (!runId) throw new Error("usage: main <runId> <task>");

const state = await runPlan(runId, task, plan);
console.log(
  `${state.completed.length}/${plan.length} steps, ` +
    `in=${state.totals.input} out=${state.totals.output}`,
);

Compile it, run it, interrupt it partway through with Ctrl-C, then run it again with the same runId

. The first process drains after the step it is in and checkpoints; the second reads the file, skips what is already done, and carries the token totals forward:

$ node main.js run-1 "audit the billing module"
^C
1/2 steps, in=27 out=61

$ node main.js run-1 "audit the billing module"
resuming run-1: 1/2 done
2/2 steps, in=109 out=704

Those numbers come from an actual run against a small cheap model, not from Astra. The line that matters is resuming run-1: 1/2 done

, not the token counts. Nothing in the runner knows or cares which model id you set.

That is the whole trick. Nothing about it is specific to Astra, or to OpenAI, or to 2026. It only becomes load-bearing when runs get long enough that starting over is expensive.

Resuming rebuilds the agent's memory of what it did. It does not undo what it did.

If step three called a payments API and the process died between the API call returning and save

committing, the checkpoint has no record of step three, and the resume will run it again. The world now has two charges.

The fix lives at the tool boundary, not the checkpoint boundary. Every effectful call carries a key derived from the run and the step, and the downstream system dedupes on it:

async function chargeCustomer(
  ctx: { runId: string },
  stepName: string,
  amountCents: number,
) {
  return stripe.paymentIntents.create(
    { amount: amountCents, currency: "eur" },
    { idempotencyKey: `${ctx.runId}:${stepName}` },
  );
}

A replayed step reaches Stripe with the key it used the first time and gets the original PaymentIntent back instead of a second charge. Stripe documents this, many payment and booking APIs support the same pattern, and any internal API you own can support it in about ten lines. What keeps you out of trouble: if a step has a side effect the outside world can see, it needs an idempotency key before it needs anything else.

The narrower version of the same rule: put at most one effectful call in a step. A step that does three writes and dies after the second is a step you cannot safely replay.

There is a second thing checkpointing does not fix, and it gets worse as runs get longer. A model that works for forty minutes with computer control spends forty minutes making decisions about content it fetched along the way, some of which it did not fetch from you. OpenAI's system card cites external evaluations putting Astra's prompt-injection attack success rate at 8.5%, against 27.0% for its predecessor. Read that the way you read any figure a vendor chooses to publish about its own model. Better than 27% is better. It is not zero, and a resumed run replays what it was told to do exactly as faithfully as it replays what you told it to do. Checkpoint the state; do not checkpoint your trust in it.

If a single run can outlive an HTTP request, it is a job. Give it a run id, decompose it into named steps, checkpoint after every step, resume by name, and put an idempotency key on anything that touches the outside world.

What changed is the duration. OpenAI is now shipping a model it says will work for forty minutes on one task, and its own reported agentic scores — 74.1% and 72.6%, on two benchmarks that are not the same benchmark — sit twenty points below its knowledge scores. Long runs fail. The cost of not having durable step state used to be a few wasted seconds. Whatever your own failure rate turns out to be, it is now paid in dollars per failed run.

Go look at your longest agent run from last week. Ask what happens to it if you deploy right now. If the answer is "it starts over," you have an afternoon of work in front of you and the code above is most of it.

Step decomposition, durable state, and resume are the spine of AI That Plans — it works through the same problem in LangGraph.js, where the checkpointer and the state graph are first-class instead of something you hand-roll. The hand-rolled version in this post is worth writing once, so that you know what the framework is doing for you.

It is book 4 of AI in TypeScript, a five-book series that runs from your first LLM call through to agents you can leave running in production.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @openai 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/openai-says-gpt-6-as…] indexed:0 read:16min 2026-09-03 ·