Skip to content

fix(kernel): a dead leased attempt serves no retry delay - #158

Merged
kjgbot merged 1 commit into
mainfrom
fix/155-crash-skips-backoff
Sep 4, 2026
Merged

fix(kernel): a dead leased attempt serves no retry delay#158
kjgbot merged 1 commit into
mainfrom
fix/155-crash-skips-backoff

Conversation

@kjgbot

@kjgbot kjgbot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Closes #155. Unblocks #134, #139 and #157, which are all red on this one test and nothing else.

What was actually wrong

Not what I first thought — I got this wrong twice before instrumenting, and both wrong answers are recorded on #155 so nobody retries them.

abandonment_actions records an attempt that died without producing a result a gate could judge: a crashed worker, or an expired lease. It already declines to charge such an attempt a semantic iteration, explicitly because the failure is not the step's own. But it still made it serve the retry backoff — the same category error. The backoff curve exists to damp a step that keeps failing on its merits, not one whose worker was killed.

That delay is what made recovery order a race:

PROBE pass now=1788503129020
  states=[("lane-b", Backoff { wake_at_ms: 1788503129025 }),
          ("lane-a", Runnable)]  due_waits=0
      → panicked at worker_capacity.rs:149

The dead lane sits in Backoff with wake_at_ms five milliseconds out. The scheduler's due_waits only fires once that passes, and due_waits is returned ahead of any starts — so whether the recovered lane or an idle sibling claimed the single free worker depended purely on when the next pass landed relative to that deadline.

The fix is one expression: a dead leased attempt is due immediately.

Evidence

failures
before, 27 runs 4 (~15%)
after, 60 runs 0

Chance of 60 clean runs at the old rate is ≈ 6e-5. It also failed on GitHub's runners, on #134 and #157 independently, so this was not local.

kernel:  130 passed, 0 failed
SDK:     464 passed, 3 skipped, 0 failed

One assertion moved — please look at this bit

parallel_tests::overlapping_agent_surfaces_are_serialized_in_authored_order failed after the change. Its intent is "the conflicting sibling must not pass an unfinished lane", but it spelled that as "every action is an ArmTimer" — which only held because there was a delay to arm a timer for. With no delay the lane is due at once, so the pass emits Append(WaitCompleted, lane-b) and no timer.

I verified what the pass actually yields rather than assuming:

PROBE Append type=WaitCompleted step=Some("lane-b")

Sole action, and critically no lane-a start — the invariant holds. So the assertion now tests the invariant directly: no action may start the sibling, and the recovered lane must be woken first. That is strictly stronger than the mechanism it replaced, but it is still me editing a test that judges my own change, so it deserves a reviewer's eye.

Scope and the known risk

@khaliqgant chose this option over two narrower ones. The accepted trade-off: a step that crashes repeatedly now retries with no delay and can hot-loop. max_iterations does not bound it, because crashed attempts deliberately do not consume the allowance. If that bites, the narrower fix is to have only an explicit run.resume preempt the backoff, leaving autonomous retry timing alone.

Needs an independent signoff at 5f26664 — I wrote it, so not mine.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Closes #155.

`abandonment_actions` records an attempt that died without producing a result
a gate could judge -- a crashed worker or an expired lease. It already declines
to charge such an attempt a semantic iteration, on the grounds that the failure
is not the step's own. It nevertheless made it serve the retry backoff, which
is the same category error: the backoff curve exists to damp a step that keeps
failing on its merits, not one whose worker was killed.

That delay also made recovery order a race. The dead lane sat in Backoff with
wake_at_ms a few milliseconds out; the scheduler's due_waits only fires once
that passes, and due_waits is returned ahead of any starts. So whether the
recovered lane or an idle sibling claimed the one free worker depended on when
the next pass happened to land:

  PROBE pass now=1788503129020
    states=[("lane-b", Backoff { wake_at_ms: 1788503129025 }),
            ("lane-a", Runnable)]  due_waits=0

Five milliseconds decided it. worker_capacity.rs:149 saw lane-a instead of the
retried lane-b in ~15% of runs (4 failures in 27), and on GitHub's runners too.
With this change: 60 consecutive passes, 0 failures.

One existing assertion moved. parallel_tests spelled "the sibling must not pass
an unfinished lane" as "every action is an ArmTimer", which held only because
there was a delay to arm a timer for. The lane is now due at once, so the pass
wakes it instead. The assertion now tests the invariant directly -- no action
may start the sibling, and the recovered lane must be woken first -- which is
strictly stronger than the mechanism it replaced.

Kernel 130 passed, SDK 464 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 41 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bfe348d0-29f7-4026-9c31-e06d12964c43

📥 Commits

Reviewing files that changed from the base of the PR and between ee28397 and 5f26664.

📒 Files selected for processing (2)
  • kernel/relayflowd-core/src/machine/parallel_tests.rs
  • kernel/relayflowd-core/src/machine/recovery.rs

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

kjgbot pushed a commit that referenced this pull request Sep 4, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
@kjgbot
kjgbot merged commit 2f2676b into main Sep 4, 2026
1 of 3 checks passed
kjgbot added a commit that referenced this pull request Sep 4, 2026
Closes #155.

`abandonment_actions` records an attempt that died without producing a result
a gate could judge -- a crashed worker or an expired lease. It already declines
to charge such an attempt a semantic iteration, on the grounds that the failure
is not the step's own. It nevertheless made it serve the retry backoff, which
is the same category error: the backoff curve exists to damp a step that keeps
failing on its merits, not one whose worker was killed.

That delay also made recovery order a race. The dead lane sat in Backoff with
wake_at_ms a few milliseconds out; the scheduler's due_waits only fires once
that passes, and due_waits is returned ahead of any starts. So whether the
recovered lane or an idle sibling claimed the one free worker depended on when
the next pass happened to land:

  PROBE pass now=1788503129020
    states=[("lane-b", Backoff { wake_at_ms: 1788503129025 }),
            ("lane-a", Runnable)]  due_waits=0

Five milliseconds decided it. worker_capacity.rs:149 saw lane-a instead of the
retried lane-b in ~15% of runs (4 failures in 27), and on GitHub's runners too.
With this change: 60 consecutive passes, 0 failures.

One existing assertion moved. parallel_tests spelled "the sibling must not pass
an unfinished lane" as "every action is an ArmTimer", which held only because
there was a delay to arm a timer for. The lane is now due at once, so the pass
wakes it instead. The assertion now tests the invariant directly -- no action
may start the sibling, and the recovered lane must be woken first -- which is
strictly stronger than the mechanism it replaced.

Kernel 130 passed, SDK 464 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Co-authored-by: kjgbot <kjgbot@agentrelay.dev>
Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
kjgbot pushed a commit that referenced this pull request Sep 4, 2026
…ucible

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1

Session-Id: c228933d-4f94-4d83-9a9a-daf3c83b94f1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P1: #137 made dispatch order to a capacity-1 worker nondeterministic (60/60 → ~15% failure)

1 participant