{"slug": "openai-says-gpt-6-astra-runs-40-minutes-on-one-task-your-agent-loop-probably-can", "title": "OpenAI Says GPT-6 Astra Runs 40 Minutes on One Task. Your Agent Loop Probably Can't.", "summary": "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.", "body_md": "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.\n\nAt 14:34 the user hits retry. The agent reads the ticket. It opens the repo. It edits four files.\n\nThirty-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.\n\nThis 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.\n\nOpenAI 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.\n\nHere are the scores OpenAI reported at launch. All of these are **vendor-reported and not independently verified** at the time of writing:\n\n| Benchmark | OpenAI-reported score |\n|---|---|\n| ARC-AGI-3 | 98.6% |\n| FrontierMath Tier 4 v2 | 97.6% |\n| GPQA Diamond | 96% |\n| BenchCAD | 95.9% |\n| DeepSWE v1.1 | 74.1% |\n| OSWorld 2.0 (offline subset) | 72.6% |\n\nOn 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](https://venturebeat.com/technology/welcome-to-the-agi-era-openai-launches-gpt-6-astra) has the full set.\n\nForty 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.\n\nGreg 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](https://venturebeat.com/technology/welcome-to-the-agi-era-openai-launches-gpt-6-astra)). 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.\n\nLook at the table again as two groups.\n\nThe 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%.\n\nReading 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.\n\nBut 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.\n\nThe published pricing makes that concrete. As of publication, [Artificial Analysis lists](https://artificialanalysis.ai/models/gpt-6-astra-high) 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.\n\nConcise 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.\n\nAlmost none of the deaths are the model's fault.\n\n**The deploy.** Rolling update, SIGTERM, `terminationGracePeriodSeconds: 30`\n\n, process gone. Your agent was 31 minutes into a 40-minute task.\n\n**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](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/edit-load-balancer-attributes.html#connection-idle-timeout) starts at 60 seconds. So does nginx [ proxy_read_timeout](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout).\n\n**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.\n\n**The platform limit.** [Lambda](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html) stops at 15 minutes. [Vercel functions](https://vercel.com/docs/functions/configuring-functions/duration) 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](https://cloud.google.com/run/docs/configuring/request-timeout) 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.\n\n**The OOM kill.** Long runs accumulate context. A 1M-token window is an invitation to accumulate a lot of it.\n\nThe 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.\n\nThe 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.\n\nThe 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.\n\nStart with the state:\n\n```\n// state.ts\nexport type Usage = { input: number; output: number };\n\nexport type StepResult = {\n  name: string;\n  output: unknown;\n  usage: Usage;\n  finishedAt: string;\n};\n\nexport type RunState = {\n  runId: string;\n  task: string;\n  completed: StepResult[];\n  totals: Usage;\n};\n\nexport function emptyRun(\n  runId: string,\n  task: string,\n): RunState {\n  return {\n    runId,\n    task,\n    completed: [],\n    totals: { input: 0, output: 0 },\n  };\n}\n```\n\n`completed`\n\nis 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.\n\n`totals`\n\nis 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.\n\nFilesystem first, because it is stdlib and it makes the durability boundary visible. The only subtlety is that a plain `writeFile`\n\nis 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`\n\n, which is atomic within a filesystem on POSIX.\n\n``` js\n// checkpoint.ts\nimport {\n  mkdir,\n  readFile,\n  rename,\n  writeFile,\n} from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RunState } from \"./state.js\";\n\nconst DIR = process.env.AGENT_STATE_DIR ?? \".agent-runs\";\n\nconst pathFor = (runId: string) =>\n  join(DIR, `${runId}.json`);\n\nexport async function save(s: RunState): Promise<void> {\n  await mkdir(DIR, { recursive: true });\n  const target = pathFor(s.runId);\n  const tmp = `${target}.${process.pid}.tmp`;\n  await writeFile(tmp, JSON.stringify(s), \"utf8\");\n  await rename(tmp, target);\n}\n```\n\nThe read side has one branch worth naming. A missing file is not an error here, it is the first run — so `ENOENT`\n\nreturns `null`\n\nand 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.\n\n```\n// checkpoint.ts, continued\n\nexport async function load(\n  runId: string,\n): Promise<RunState | null> {\n  try {\n    const raw = await readFile(pathFor(runId), \"utf8\");\n    return JSON.parse(raw) as RunState;\n  } catch (err) {\n    const code = (err as NodeJS.ErrnoException).code;\n    if (code === \"ENOENT\") return null;\n    throw err;\n  }\n}\n```\n\nIn 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`\n\nreturns 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.\n\nDo not put the state file on the pod's ephemeral disk in a Kubernetes deployment. The whole point is surviving the pod.\n\nA step takes the outputs of the steps before it and returns its own output plus its token usage.\n\n``` python\n// steps.ts\nimport type { Usage } from \"./state.js\";\n\nexport type Ctx = {\n  runId: string;\n  task: string;\n  prior: Record<string, unknown>;\n};\n\nexport type Step = {\n  name: string;\n  run: (ctx: Ctx) => Promise<{\n    output: unknown;\n    usage: Usage;\n  }>;\n};\n```\n\nThe runner is short, and every line of it is doing something. The first half rebuilds whatever the previous process left behind:\n\n``` js\n// run.ts\nimport { load, save } from \"./checkpoint.js\";\nimport { emptyRun, type RunState } from \"./state.js\";\nimport type { Step } from \"./steps.js\";\n\nlet draining = false;\nconst drain = () => {\n  draining = true;\n};\nprocess.on(\"SIGTERM\", drain);\nprocess.on(\"SIGINT\", drain);\n\nexport async function runPlan(\n  runId: string,\n  task: string,\n  plan: Step[],\n): Promise<RunState> {\n  const state =\n    (await load(runId)) ?? emptyRun(runId, task);\n\n  const done = new Set(\n    state.completed.map((s) => s.name),\n  );\n  const prior: Record<string, unknown> = {};\n  for (const s of state.completed) {\n    prior[s.name] = s.output;\n  }\n\n  if (done.size > 0) {\n    console.log(\n      `resuming ${runId}: ${done.size}/${plan.length} done`,\n    );\n  }\n```\n\nThat 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.\n\nThe second half is the loop:\n\n``` js\n// run.ts, continued — still inside runPlan\n\n  for (const step of plan) {\n    if (done.has(step.name)) continue;\n    if (draining) return state;\n\n    const { output, usage } = await step.run({\n      runId,\n      task,\n      prior,\n    });\n\n    prior[step.name] = output;\n    state.completed.push({\n      name: step.name,\n      output,\n      usage,\n      finishedAt: new Date().toISOString(),\n    });\n    state.totals.input += usage.input;\n    state.totals.output += usage.output;\n\n    await save(state);\n  }\n\n  return state;\n}\n```\n\nThree lines carry the whole design.\n\n`if (done.has(step.name)) continue;`\n\nis the resume. Work you already paid for is never paid for twice.\n\n`if (draining) return state;`\n\nis 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`\n\nto 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.\n\n`await save(state)`\n\nsitting 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.\n\nEverything 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`\n\nreturns 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.\n\n``` python\n// plan.ts  — npm i openai\nimport OpenAI from \"openai\";\nimport type { Step } from \"./steps.js\";\n\n// Reads OPENAI_API_KEY from the environment.\nconst client = new OpenAI();\n\n// Set AGENT_MODEL to the id your provider exposes.\nconst MODEL = process.env.AGENT_MODEL;\nif (!MODEL) throw new Error(\"set AGENT_MODEL\");\n\nasync function ask(prompt: string) {\n  const r = await client.responses.create({\n    model: MODEL,\n    input: prompt,\n  });\n  return {\n    output: r.output_text,\n    usage: {\n      input: r.usage?.input_tokens ?? 0,\n      output: r.usage?.output_tokens ?? 0,\n    },\n  };\n}\n```\n\nThe 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`\n\n— which is the only coupling between steps that exists.\n\n``` js\n// plan.ts, continued\n\nexport const plan: Step[] = [\n  {\n    name: \"decompose\",\n    run: ({ task }) =>\n      ask(\n        `Break this into 4 concrete subtasks. ` +\n          `One per line, no prose.\\n\\n${task}`,\n      ),\n  },\n  {\n    name: \"draft\",\n    run: ({ task, prior }) =>\n      ask(\n        `Task: ${task}\\n` +\n          `Subtasks:\\n${prior.decompose}\\n\\n` +\n          `Produce the deliverable.`,\n      ),\n  },\n];\n```\n\nAdd `research`\n\nbefore the draft and `review`\n\nafter it the same way: a name, a function, a read from `prior`\n\n. 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.\n\nThe entry point wires it together and prints the accounting, so you can watch the totals carry across a restart:\n\n``` js\n// main.ts\nimport { plan } from \"./plan.js\";\nimport { runPlan } from \"./run.js\";\n\nconst runId = process.argv[2];\nconst task = process.argv[3] ?? \"\";\n\nif (!runId) throw new Error(\"usage: main <runId> <task>\");\n\nconst state = await runPlan(runId, task, plan);\nconsole.log(\n  `${state.completed.length}/${plan.length} steps, ` +\n    `in=${state.totals.input} out=${state.totals.output}`,\n);\n```\n\nCompile it, run it, interrupt it partway through with Ctrl-C, then run it again with the same `runId`\n\n. 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:\n\n``` bash\n$ node main.js run-1 \"audit the billing module\"\n^C\n1/2 steps, in=27 out=61\n\n$ node main.js run-1 \"audit the billing module\"\nresuming run-1: 1/2 done\n2/2 steps, in=109 out=704\n```\n\nThose 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`\n\n, not the token counts. Nothing in the runner knows or cares which model id you set.\n\nThat 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.\n\nResuming rebuilds the agent's memory of what it did. It does not undo what it did.\n\nIf step three called a payments API and the process died between the API call returning and `save`\n\ncommitting, the checkpoint has no record of step three, and the resume will run it again. The world now has two charges.\n\nThe 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:\n\n```\nasync function chargeCustomer(\n  ctx: { runId: string },\n  stepName: string,\n  amountCents: number,\n) {\n  return stripe.paymentIntents.create(\n    { amount: amountCents, currency: \"eur\" },\n    { idempotencyKey: `${ctx.runId}:${stepName}` },\n  );\n}\n```\n\nA 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](https://docs.stripe.com/api/idempotent_requests), 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.\n\nThe 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.\n\nThere 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](https://deploymentsafety.openai.com/gpt-6-astra) 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.\n\nIf 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.\n\nWhat 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.\n\nGo 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.\n\nStep decomposition, durable state, and resume are the spine of *AI That Plans* — it works through the same problem in [LangGraph.js](https://langchain-ai.github.io/langgraphjs/), where the [checkpointer](https://langchain-ai.github.io/langgraphjs/concepts/persistence/) 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.\n\nIt 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.", "url": "https://wpnews.pro/news/openai-says-gpt-6-astra-runs-40-minutes-on-one-task-your-agent-loop-probably-can", "canonical_source": "https://dev.to/gabrielanhaia/openai-says-gpt-6-astra-runs-40-minutes-on-one-task-your-agent-loop-probably-cant-2hn7", "published_at": "2026-09-03 21:22:44+00:00", "updated_at": "2026-09-03 21:53:48.916446+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "ai-products"], "entities": ["OpenAI", "GPT-6 Astra", "Greg Brockman", "VentureBeat", "Artificial Analysis", "OSWorld 2.0", "ARC-AGI-3", "FrontierMath"], "alternates": {"html": "https://wpnews.pro/news/openai-says-gpt-6-astra-runs-40-minutes-on-one-task-your-agent-loop-probably-can", "markdown": "https://wpnews.pro/news/openai-says-gpt-6-astra-runs-40-minutes-on-one-task-your-agent-loop-probably-can.md", "text": "https://wpnews.pro/news/openai-says-gpt-6-astra-runs-40-minutes-on-one-task-your-agent-loop-probably-can.txt", "jsonld": "https://wpnews.pro/news/openai-says-gpt-6-astra-runs-40-minutes-on-one-task-your-agent-loop-probably-can.jsonld"}}