Repository navigation
AI runs: a run a person asks for again gets its attempt budget back (Q40 follow-up) - #917
NewWorldVenture wants to merge 5 commits into
Conversation
`attempt` is the run's abandonment budget. At `max_attempts` the recovery arm of claim_ai_runs dead-letters the run and claimRun refuses to lease it. Four writes put a run back to `ready` because a member of the family asked: resume, re-run a step, edit a step (controls.ts) and an approval decision (services/approvals). Each told the person Bubaly would pick the run up; none touched `attempt`. For a run that had spent its budget the kick answered claimed:false in silence and the next tick abandoned it again. "Try that step again" on an abandoned run produced a run abandoned again; an approval granted to a run parked at the ceiling was consumed by a run that then died. store.freshBudget() is the attempt:0 each of those writes now spreads. A human asking again is a new budget, the way a slice that made progress is (executor budgetReset). The Q40 pin that said the executor's park was the only reset now names both helpers and still forbids a bare reset anywhere else (intake, kick, cron route, controls, approvals). Stacked on the Q40 second-half branch for the claim-pass stand-in. Tests: new a-run-a-person-asks-for-again-gets-its-attempts-back (5): re-run on an abandoned run is claimed by the kick and taken by the claim pass; resume from paused at the ceiling is claimable; control row at the ceiling without a person is refused and abandoned; source pin on every ready write. run-controls +3 payload assertions, approval-execution drives the decision on a run at the ceiling. Controls: dropping freshBudget from rerunStep fails 4; from the decision fails 2. tsc, eslint clean; 15 scanner suites / 316 cases green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…uns-human-restart-budget-20261003 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Independent review of exact head The new tests prove a previously abandoned run can be resumed with a fresh budget, but the source-shape assertion that every ready patch contains |
…uns-human-restart-budget-20261003
Review on #917 (5973301004): the four writes that put a run back in the queue because a person asked — an approval decision folding into its run, resume, re-run a step, edit a step — wrote `ready`, a fresh budget and a cleared lease through the plain `updateRun`, which matches on id and family alone. Two of them can hold the same snapshot. Two final approval folds on one run: the first puts it in the queue, a worker claims it (`executing`, attempt charged, a live lease), and the second — still holding the snapshot that said `awaiting_approval` — restored `ready`, zeroed the attempt and cleared the worker's live lease while it executed. The lease fence #901 gave the worker does not reach a write a person's control makes. `store.updateRunAsObserved` is the compare-and-set on the run as the caller observed it — state, attempt and lease together, not the state alone, because a run can be `executing` before and after under two different workers. The fold and the three controls write through it. Zero rows is "someone else moved this run first": the fold has nothing left to do (the steps it released are in the graph the running slice reloads every pass) and sends no kick; resume reports the run is not paused; re-run and edit say the run changed and nothing was overwritten (the re-queued step is the claim's to pick up; the saved plan version stays unreferenced). The attempts-back test's source case now counts the observed writes and pins that no bare `ready` write remains. tests/a-stale-fold-cannot-re-arm-a-claimed-run.test.ts (6): a client that hands the re-entry its snapshot of the run and, before it can act, lets the other party move — the real `decide` and `claimRun` over the same in-memory tables. The two-fold/claim race (B's fold leaves the worker's claim, attempt and lease untouched, releases its step, sends no kick; one kick in total); a fold whose snapshot holds lands with a fresh budget; re-run a step against a claim; re-run a step on an executing run whose slice parked and was re-claimed meanwhile (same state, different attempt and lease); resume against another tab's resume and a claim; a control whose snapshot holds lands. Mutation controls, files restored and checksummed: the fold back on `updateRun` 1 failure; the controls back on `updateRun` 2; the compare on state alone 1. 78 run-spine suites (1159) green, three time-sensitive files also under TZ=America/Los_Angeles; tsc and eslint clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
Confirmed, and closed in The guard. The deterministic race (
Mutation controls, files restored and checksummed after each: the fold back on Evidence is the in-memory harness and source, as before; no PostgreSQL race test or production execution is claimed. Generated by Claude Code |
…uns-human-restart-budget-20261003
Stacked on #904 (base
claude/ai-runs-claim-ceiling-20261003) for the claim-pass stand-in intests/helpers/claim-ai-runs.ts. Retarget as the stack lands. No migration.What was wrong
attemptis the run's abandonment budget. Atmax_attemptsthe recovery arm ofclaim_ai_runsdead-letters the run (#904) andclaimRunrefuses to lease it (already on main). Four writes put a run back toreadybecause a member of the family asked for it:resumeRun,rerunStep,editStepInputinlib/ai/runs/controls.tslib/services/approvals/index.tsEach one tells the person Bubaly will pick the run up. None touched
attempt. For a run that had spent its budget that promise failed twice: the kick after the control went throughclaimRun, which answeredclaimed: falsewithout a word, and the next cron tick abandoned the run again. "Try that step again" on an abandoned run produced a run abandoned again, with nothing a person could do about it. An approval granted to a run parked at the ceiling was consumed by a run that then died.Change
freshBudget()inlib/ai/runs/store.tsreturns{ attempt: 0 }. The four writes above spread it. A human asking again is a new budget, the way a slice that made progress is (budgetResetin the executor).tests/a-successful-slice-must-not-spend-a-retry.test.tsthat said the executor's park was the only reset now names both helpers and still forbids a bareattempt: 0in the intake, the kick, the cron route, the controls and the approvals service.Second commit — review fix (
a66ac1269, review 5973301004)The four re-entry writes went through the plain
updateRun, which matches on id and family alone, so two of them holding the same snapshot both landed: two final approval folds on one run — the first puts it in the queue, a worker claims it (executing, attempt charged, a live lease), and the second, still holding the snapshot that saidawaiting_approval, restoredready, zeroed the attempt and cleared the worker's live lease.resumeRunandrerunStepwrote from their snapshots the same way. The lease fence #901 gave the worker does not reach a write a person's control makes.store.updateRunAsObserved(scope, runId, observed, patch)— compare-and-set onstate,attemptandlease_owneras the caller observed them (.is('lease_owner', null)when the snapshot had none). Not the state alone: a run can beexecutingbefore and after under two different workers.foldIntoRun,resumeRun,rerunStep,editStepInputwrite through it. Zero rows is "someone else moved this run first": the fold has nothing left to do (its released steps are in the graph the running slice reloads every pass) and sends no kick; resume reports the run is not paused; re-run and edit say the run changed and nothing was overwritten (the re-queued step is the claim's to pick up; the saved plan version stays unreferenced). The source case in the attempts-back test counts the four observed writes and pins that no barereadywrite remains.tests/a-stale-fold-cannot-re-arm-a-claimed-run.test.ts(6) — the in-memory client wrapped so the first run read hands the re-entry its snapshot and, before it can act, runs the other party (the realdecideandclaimRunover the same tables): the two-fold/claim race (B's step released, the worker's claim/attempt/lease untouched, one kick in total); a fold whose snapshot holds lands with the fresh budget; re-run a step against a claim; re-run a step on an executing run whose slice parked and was re-claimed meanwhile (same state, different attempt and lease); resume against another tab's resume plus a claim; a control whose snapshot holds lands.Tests
tests/a-run-a-person-asks-for-again-gets-its-attempts-back.test.ts(5 cases). Real controls, realclaimRunand the claim-pass stand-in against the in-memory client: a step re-run on an abandoned run is claimed by the kick and taken by the claim pass instead of being dead-lettered; a resume from paused at the ceiling is claimable; the control row at the ceiling with no person asking is refused by the kick and abandoned by the tick, which is what a person's run met before; a source pin that everyreadywrite in both files carries the fresh budget (and, since the second commit, is the observed compare-and-set).tests/run-controls.test.ts: the three payload assertions gainattempt: 0.tests/approval-execution.test.ts: the approve case now parks the run at the ceiling and asserts the budget is back.Evidence
freshBudgetfromrerunStepfails 4 cases; dropping it from the decision fails 2. Typecheck and eslint clean. The 15 suites that scan these source files or the tests directory pass (316 cases).updateRun1 failure; the controls back onupdateRun2; the compare on state alone 1. 78 run-spine suites (1159 tests) green, the time-sensitive files also underTZ=America/Los_Angeles;tscand ESLint clean. In-memory harness and source only; no PostgreSQL race test is claimed.Not done
pauseRunandcancelRunkeep the plain write: neither hands back a budget or clears a lease that is not already the person's to clear.🤖 Generated with Claude Code
https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
Generated by Claude Code