JOB_APPLY is the last autonomous workflow with no budget pool
#502 closed the three coding entry points (f79fb29) — coding_loop_start, POST …/sessions/:sid/run, and the Overseer's delegation in coding-brains.ts, which the ticket had not named. JOB_APPLY was found in the same pass and deliberately left out of scope, because it needs wiring rather than an argument.
#184 promised an admission check at every autonomous entry point. This is the one that is left.
Verified
$ grep -rn "reserve(" workers/api/src | grep -v '\.test\.'
workers/api/src/workflows/coding-session.ts:466 const draw = await reserve(env, userId, budgetId, {
workers/api/src/workflows/agent-loop.ts:88 reserve(this.env, userId, budgetId, { depth, estimatedCostMicros: reserveMicros }),
workers/api/src/lib/delegation-budget-store.ts:283 export async function reserve(
workers/api/src/lib/connectors/supervision.ts:626 // …comment only
Two call sites. Neither is the apply path.
$ grep -c "budget" workers/api/src/workflows/job-apply.ts
0
The workflow does not mention a budget at all — there is no pool to draw from, nothing to settle, and no budgetId to cancel against.
Why it matters as much as the coding ones did
JobApplyWorkflow is a durable Cloudflare Workflow driving a real browser with the owner's BYOK Claude: snapshot → Claude picks one action → act → repeat. It is long-running by design (that is why it is a Workflow rather than a DO request), it is resumable across handoffs, and each iteration is a model call. It is exactly the shape the pool exists for.
The concrete consequence is the one #291's lineage already demonstrated for the Pilot: a run with no pool has no row for requestCancel to act on. An apply run that wedges — on a captcha handoff, a needs_input nobody answers, or an ATS that never settles — spends tokens the owner can neither see attributed nor stop.
Note this is independent of enforcement posture. BUDGET_ENFORCE is currently observe-only by design (d951b5f), so a pool here is about accounting and cancellability, not refusal. Do not treat "enforcement is off" as a reason to skip it.
Acceptance criteria
startJobApply opens a pool and threads budgetId into JobApplyWorkflow, the way coding-session-params.ts does for CODING_SESSION.
- Each model iteration in the apply loop draws against it, and the run settles on every exit path — including the handoff paths (
challenge, stuck, needs_input), which is where an apply run actually stops.
- A wedged or abandoned apply run does not leak its reservation permanently. Note
agent-loop's settle was swallowed once and caused exactly that leak (fixed in 50e56ed).
workers/api/src/lib/autonomous-budget.test.ts — added by f79fb29 and non-vacuous — is extended so job-apply.ts is in the guarded set. That file already encodes the invariant; this should become one more entry in it, not a parallel test.
usage_summary attributes apply spend to the run, so it is legible next to the coding rows.
Not verified
I have not run an apply end to end — that needs a live runner and a real job URL. The absence of any budget wiring is verified from the source above; the consequence (a wedged run that cannot be cancelled) is inferred from the identical mechanism in the Pilot, which was observed.
Related, kept separate
f79fb29 also found that coding_loop_start / check / stop duplicate start_instance_loop / check_instance_loop / stop_instance_loop — six MCP tools doing the same thing under two names. It rewired rather than deleted, because AC 2 wanted the name to keep working. Collapsing them is a deliberate call and belongs in its own ticket, not here.
JOB_APPLYis the last autonomous workflow with no budget pool#502 closed the three coding entry points (
f79fb29) —coding_loop_start,POST …/sessions/:sid/run, and the Overseer's delegation incoding-brains.ts, which the ticket had not named.JOB_APPLYwas found in the same pass and deliberately left out of scope, because it needs wiring rather than an argument.#184 promised an admission check at every autonomous entry point. This is the one that is left.
Verified
Two call sites. Neither is the apply path.
The workflow does not mention a budget at all — there is no pool to draw from, nothing to settle, and no
budgetIdto cancel against.Why it matters as much as the coding ones did
JobApplyWorkflowis a durable Cloudflare Workflow driving a real browser with the owner's BYOK Claude: snapshot → Claude picks one action → act → repeat. It is long-running by design (that is why it is a Workflow rather than a DO request), it is resumable across handoffs, and each iteration is a model call. It is exactly the shape the pool exists for.The concrete consequence is the one #291's lineage already demonstrated for the Pilot: a run with no pool has no row for
requestCancelto act on. An apply run that wedges — on a captcha handoff, aneeds_inputnobody answers, or an ATS that never settles — spends tokens the owner can neither see attributed nor stop.Note this is independent of enforcement posture.
BUDGET_ENFORCEis currently observe-only by design (d951b5f), so a pool here is about accounting and cancellability, not refusal. Do not treat "enforcement is off" as a reason to skip it.Acceptance criteria
startJobApplyopens a pool and threadsbudgetIdintoJobApplyWorkflow, the waycoding-session-params.tsdoes forCODING_SESSION.challenge,stuck,needs_input), which is where an apply run actually stops.agent-loop's settle was swallowed once and caused exactly that leak (fixed in50e56ed).workers/api/src/lib/autonomous-budget.test.ts— added byf79fb29and non-vacuous — is extended sojob-apply.tsis in the guarded set. That file already encodes the invariant; this should become one more entry in it, not a parallel test.usage_summaryattributes apply spend to the run, so it is legible next to the coding rows.Not verified
I have not run an apply end to end — that needs a live runner and a real job URL. The absence of any budget wiring is verified from the source above; the consequence (a wedged run that cannot be cancelled) is inferred from the identical mechanism in the Pilot, which was observed.
Related, kept separate
f79fb29also found thatcoding_loop_start/check/stopduplicatestart_instance_loop/check_instance_loop/stop_instance_loop— six MCP tools doing the same thing under two names. It rewired rather than deleted, because AC 2 wanted the name to keep working. Collapsing them is a deliberate call and belongs in its own ticket, not here.