You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A 2-hour run that closed ten issues and pushed 15 times is reported as "outcome: failed" with a Wrangler docs link — the Cloudflare subrequest ceiling is relayed raw as the objective failing #523
His Coder Lead delegated "Work through all open GitHub issues by priority order — bugs first, then enhancements". Two hours later the Assistant thread said, in full:
Loop stopped (failed)
The board card says more, and it is worse:
Failed · deleg-ae7fa3f2-53a1-43ba-b3ad-921dd8dba7b1 outcome: failed — run error: Too many API requests by single Worker invocation. To configure this limit, refer to https://developers.cloudflare.com/workers/wrangler/configuration/#limits | Acts: pushed directly to the trunk origin main; pushed directly to the trunk origin main; pushed directly to th…
Read from the owner's side: his objective failed, and the remedy is a Cloudflare Wrangler configuration page for a Worker he does not own. There is no sentence anywhere telling him that the run had already implemented and pushed most of the backlog before it died.
There is exactly one classified escape — isRunnerGone, from #341. Everything else, including a Cloudflare runtime limit, becomes run error: <raw platform string> and is then the detail on the board card, the loop-run row and the chat message.
Verified. The run's dominant subrequest consumer is the idle poll. coding-session.ts:497-509:
// Poll capture until the CLI goes idle … Bounded so the loop can't outrun idleRetry's 10-minute step timeout.
waitIdle: ()=>guard(runIdle,`s${n++}-waitidle`,async()=>{awaitsleep(1500);letsnap=awaitcapture();for(letpoll=0;poll<240&&snap.runState!=="idle"&&snap.alive&&!snap.cancelled;poll++){awaitsleep(2000);snap=awaitcapture();}returnsnap;})
sleep is a plain setTimeout promise (coding-session.ts:795) — notstep.sleep. So up to 241 capture() calls happen inside a single step.do. Each capture() (coding-session.ts:383-431) issues, at minimum: one relay callRunner for /coding/capture, one D1 SELECT status FROM coding_sessions, and one isCancelRequested D1 read — plus recordEngineUsage, recordEngineActs, recordAuthorityViolations and touchSessionActivity, all of which reach D1. That is ≥3 and typically 5–7 subrequests per poll, so one waitIdle can spend 700–1700 subrequests. A message action calls waitIdle every step (coding-loop.ts, end of the action branch), 26 times in this run.
Inferred, and labelled as such: that this is where the invocation's budget went. I have not instrumented a live Workflow to attribute the exhausted counter to a specific step, and Cloudflare's step/invocation boundary semantics are not something I verified from the runtime. What is verified is that (a) the ceiling was hit, (b) the platform already knows this ceiling — workers/api/src/lib/repo-ingest-runner.ts:252 describes an incident where "ONE tick drained a 300-file queue (blowing past the 1,000-subrequest cap, which embed's catch swallows)" and a chunk budget was added specifically to stay under it — and (c) workers/api/wrangler.toml declares no [limits] block, so the API Worker runs at whatever the default is.
What to do
1. Say what happened, in the platform's own voice — the cheapest and most valuable step.
Follow the pattern #341 already established in this exact catch block: a failure that KNOWS what it is says so, instead of being inferred from a message at the catch site. Add a classifier beside isRunnerGone for a platform-ceiling error (Too many API requests by single Worker invocation, Too many subrequests, exceeded CPU) and emit something the owner can act on:
This run reached a Cloudflare per-invocation limit after 2h and 26 steps. It was not your objective failing — the work it had already committed and pushed is intact (15 pushes to main). Resume it, or narrow the objective.
Crucially it must not be reported as outcome: failed with no qualification: a delegating supervisor reads check_delegation and the board card, and both currently say the objective could not be done.
2. Stop spending the budget in one invocation. Break waitIdle into bounded chunks: poll ~15 times inside a step, then await step.sleep(...) and continue. step.sleep is already used in this very file for the handoff wait (coding-session.ts:650), for the same reason — a long wait must not be held open inside one invocation. This also makes a long engine turn survive a Workflow eviction instead of burning the retry budget.
3. Consider raising the ceiling explicitly, but not instead of (2).wrangler.toml supports [limits] subrequests (Cloudflare docs: default 10,000 on paid, maximum 10,000,000). Adding it is one line. It is deliberately listed third: raising the ceiling alone means the next run of this shape goes further unsupervised before it dies — a run that pushed to main 15 times without a human reading anything is not obviously improved by letting it push 30 times.
4. Checkpoint, so a cut-off run is resumable. The Pilot's progress lives entirely in the run's in-memory actionLog/transcript (coding-loop.ts). When the invocation dies, so does the plan. A run that has closed 8 of 12 issues should be resumable from issue 9, not from the objective.
Alternatives rejected
Just raise [limits] subrequests and close this. Fixes the symptom for a run of this length and leaves the message, the outcome classification and the unresumability exactly as they are. It also does nothing for the CPU-time ceiling, which the same wrapper will report the same way.
Cap maxSteps lower. A 26-step run that closes ten issues is the product working. The defect is that the platform cannot tell the owner what happened to it.
Poll less often. 2s is what makes the terminal feel live and is what touchSessionActivity is throttled against. The problem is the poll's placement inside one invocation, not its frequency.
Regression risk
Chunking waitIdle across step.sleep boundaries adds durable steps to every engine turn — n (the step-name counter) grows, and a Workflow step name must stay unique within an instance, which this file already manages via s${n++}. The risk to watch is a long engine turn now spanning many steps and colliding with the Workflow's own step-count limits; test with a synthetic engine that stays non-idle for the full 8-minute window and assert the loop still terminates at the same boundary with the same snapshot.
Not reproduced end-to-end: I did not force a subrequest exhaustion in a live Workflow. Everything above is read off the production record of one run plus the code paths it took.
Related: #341 (the precedent — a connectivity failure classifies itself instead of being guessed from its message at the catch site).
What the owner saw
His Coder Lead delegated "Work through all open GitHub issues by priority order — bugs first, then enhancements". Two hours later the Assistant thread said, in full:
The board card says more, and it is worse:
Read from the owner's side: his objective failed, and the remedy is a Cloudflare Wrangler configuration page for a Worker he does not own. There is no sentence anywhere telling him that the run had already implemented and pushed most of the backlog before it died.
What actually happened
Session
csess_e80b6a21-a154-4905-b689-fe5c5cd16289, instancebd43f4de-ef35-4051-bdec-43f8571414a1, nodeRLs-MacBook-Air.local,2026-08-11 01:29:42 → 03:31:35,status: error.Measured from
GET /v1/instances/:id/coding/sessions/:sid/timelineandagent_trace:act.consequential/push.trunkevents, eachirreversible: true, ok: true.The run did not fail. It was cut off at 2 hours by a platform ceiling, and the only thing it left behind that a human reads says "failed".
Mechanism
Verified.
workers/api/src/workflows/coding-session.ts:669-677wraps the whole loop:There is exactly one classified escape —
isRunnerGone, from #341. Everything else, including a Cloudflare runtime limit, becomesrun error: <raw platform string>and is then thedetailon the board card, the loop-run row and the chat message.Verified. The run's dominant subrequest consumer is the idle poll.
coding-session.ts:497-509:sleepis a plainsetTimeoutpromise (coding-session.ts:795) — notstep.sleep. So up to 241capture()calls happen inside a singlestep.do. Eachcapture()(coding-session.ts:383-431) issues, at minimum: one relaycallRunnerfor/coding/capture, one D1SELECT status FROM coding_sessions, and oneisCancelRequestedD1 read — plusrecordEngineUsage,recordEngineActs,recordAuthorityViolationsandtouchSessionActivity, all of which reach D1. That is ≥3 and typically 5–7 subrequests per poll, so onewaitIdlecan spend 700–1700 subrequests. Amessageaction callswaitIdleevery step (coding-loop.ts, end of the action branch), 26 times in this run.Inferred, and labelled as such: that this is where the invocation's budget went. I have not instrumented a live Workflow to attribute the exhausted counter to a specific step, and Cloudflare's step/invocation boundary semantics are not something I verified from the runtime. What is verified is that (a) the ceiling was hit, (b) the platform already knows this ceiling —
workers/api/src/lib/repo-ingest-runner.ts:252describes an incident where "ONE tick drained a 300-file queue (blowing past the 1,000-subrequest cap, which embed's catch swallows)" and a chunk budget was added specifically to stay under it — and (c)workers/api/wrangler.tomldeclares no[limits]block, so the API Worker runs at whatever the default is.What to do
1. Say what happened, in the platform's own voice — the cheapest and most valuable step.
Follow the pattern #341 already established in this exact catch block: a failure that KNOWS what it is says so, instead of being inferred from a message at the catch site. Add a classifier beside
isRunnerGonefor a platform-ceiling error (Too many API requests by single Worker invocation,Too many subrequests, exceeded CPU) and emit something the owner can act on:Crucially it must not be reported as
outcome: failedwith no qualification: a delegating supervisor readscheck_delegationand the board card, and both currently say the objective could not be done.2. Stop spending the budget in one invocation. Break
waitIdleinto bounded chunks: poll ~15 times inside a step, thenawait step.sleep(...)and continue.step.sleepis already used in this very file for the handoff wait (coding-session.ts:650), for the same reason — a long wait must not be held open inside one invocation. This also makes a long engine turn survive a Workflow eviction instead of burning the retry budget.3. Consider raising the ceiling explicitly, but not instead of (2).
wrangler.tomlsupports[limits] subrequests(Cloudflare docs: default 10,000 on paid, maximum 10,000,000). Adding it is one line. It is deliberately listed third: raising the ceiling alone means the next run of this shape goes further unsupervised before it dies — a run that pushed tomain15 times without a human reading anything is not obviously improved by letting it push 30 times.4. Checkpoint, so a cut-off run is resumable. The Pilot's progress lives entirely in the run's in-memory
actionLog/transcript(coding-loop.ts). When the invocation dies, so does the plan. A run that has closed 8 of 12 issues should be resumable from issue 9, not from the objective.Alternatives rejected
[limits] subrequestsand close this. Fixes the symptom for a run of this length and leaves the message, the outcome classification and the unresumability exactly as they are. It also does nothing for the CPU-time ceiling, which the same wrapper will report the same way.maxStepslower. A 26-step run that closes ten issues is the product working. The defect is that the platform cannot tell the owner what happened to it.touchSessionActivityis throttled against. The problem is the poll's placement inside one invocation, not its frequency.Regression risk
Chunking
waitIdleacrossstep.sleepboundaries adds durable steps to every engine turn —n(the step-name counter) grows, and a Workflow step name must stay unique within an instance, which this file already manages vias${n++}. The risk to watch is a long engine turn now spanning many steps and colliding with the Workflow's own step-count limits; test with a synthetic engine that stays non-idle for the full 8-minute window and assert the loop still terminates at the same boundary with the same snapshot.Not reproduced end-to-end: I did not force a subrequest exhaustion in a live Workflow. Everything above is read off the production record of one run plus the code paths it took.
Related: #341 (the precedent — a connectivity failure classifies itself instead of being guessed from its message at the catch site).