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
{{ message }}
Repository navigation
control.t27: bound how often one task re-runs a paid model call across crashes #7059
control.t27 section 7 (#7006) re-runs a model call whose intent is open when its holder crashed: effect_action(EFF_INTENT, EK_MODEL_CALL, ...) is DO_RUN, because a model call cannot be looked up. Each crash that leaves the intent open pays for the call again. Nothing checks how many times that can happen for one task:
actors.t27 bounds restarts of one runtime: DOMAIN_MAX_RESTARTS 3 per DOMAIN_PERIOD_SECONDS 300, and UNSTABLE_GIVE_UP 6 short runs. A run longer than STABLE_SECONDS resets the streak, and a reclaimed task can move to another runtime, so neither bound counts per task.
dispatch.t27 states the per-issue bound only as prose: RETRY_RULE says three free attempts with nothing judgeable escalate. counts_against_issue(true) is false, so an interrupted attempt is not a counted one.
dispatch.t27 RETRY_RULE and counts_against_issue (lines printed by grep -n 'RETRY_RULE\|counts_against_issue' specs/queen/dispatch.t27).
Prior art: Temporal bounds an activity's retries with maximumAttempts in its retry policy, and Restate's ctx.run takes retry options with a maximum number of attempts. Both count per step, not per worker.
Proposal
Add a pub fn to control.t27, model_call_may_rerun(open_intent_reruns: u32) -> bool, true below a bound. The bound must be the same number dispatch.t27's RETRY_RULE gives free attempts (3); per T7, write it as a literal with a comment naming its owner, not as an import. Past the bound, the journal answers an escalation instead of DO_RUN. A test pins the bound with runtime asserts, and an invariant ties the per-task bound to at most the free-attempt count.
Implemented as local commit f8119a488 on claude/queen-actors-next2 (control.t27 section 7). It will land in one PR after #7060 merges. Three changes against this issue's text, and why:
No "escalate". Past the limit the answer is DO_GIVE_UP: the attempt ends as a failure that counts against the issue (give_up_counts_against_issue() == true). Then dispatch.t27's send-back ceiling and review_valve.t27 take it automatically. An escalation would be a hold for a person, which the owner's rule (no person in the loop) forbids. A give-up is the opposite of a cancel (cancel_counts_against_issue() == false): dispatch.t27 does not count an interruption, so without a counted give-up the loop around the effect would still have no bound.
Every effect kind, not only a model call. A push or a comment that is looked up and never found would otherwise run again on every crash too. A lookable effect is still looked up first at any count, so one that landed is skipped, not given up.
The count cannot be reset by a new holder. It is written with each intent (runs_after_intent), before the effect runs, so a crash during the effect counts.
Temporal's Maximum Attempts counts runs, and "The default is unlimited."
EFFECT_RUN_LIMIT is 3. It counts runs, the first included, as Temporal does. Three is the number of free attempts dispatch.t27 RETRY_RULE gives an issue, written as a literal per T7. An invariant keeps it in 2..3: one crash must not end an attempt, and one key never gets more runs than an issue gets attempts.
Checks, printed by commands:
control.t27: 45 pub functions, 9 invariants, 16 tests; 16/16 pass via t27c gen + zig test, 0 vacuous; all 5 backends exit 0.
Hand mutants: 22 of 22 killed. Controls: a comment edit survives, and the invariant alone kills a limit of 4 when the tests are moved to 4.
Activity
gHashTag commentedon Oct 6, 2026
Implemented as local commit
f8119a488onclaude/queen-actors-next2(control.t27 section 7). It will land in one PR after #7060 merges. Three changes against this issue's text, and why:DO_GIVE_UP: the attempt ends as a failure that counts against the issue (give_up_counts_against_issue() == true). Then dispatch.t27's send-back ceiling and review_valve.t27 take it automatically. An escalation would be a hold for a person, which the owner's rule (no person in the loop) forbids. A give-up is the opposite of a cancel (cancel_counts_against_issue() == false): dispatch.t27 does not count an interruption, so without a counted give-up the loop around the effect would still have no bound.runs_after_intent), before the effect runs, so a crash during the effect counts.MaxAttemptscounts retries, default 3.EFFECT_RUN_LIMITis 3. It counts runs, the first included, as Temporal does. Three is the number of free attempts dispatch.t27RETRY_RULEgives an issue, written as a literal per T7. An invariant keeps it in 2..3: one crash must not end an attempt, and one key never gets more runs than an issue gets attempts.Checks, printed by commands:
t27c gen+zig test, 0 vacuous; all 5 backends exit 0.spec(queen): bound an effect's re-runs; close the mutation gaps in fi…
spec(queen): review_valve names an effect give-up instead of reading …