Repository navigation
fix: follow lost worker leases and retried attempts instead of abandoning the run (customer issue 1) - #593
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (12)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe daemon now sends lease durations with dispatches and heartbeat responses. The SDK uses monotonic time to track worker leases, follows running and backoff states, retries authored roots after lease loss, and includes uncharged attempts in step failure details. ChangesLease handling and recovery
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant Caller
participant AuthoredRoot
participant Kernel
AuthoredRoot->>Kernel: execute authored root
Kernel-->>AuthoredRoot: report lease loss
AuthoredRoot->>Kernel: wait for same-run redispatch
Kernel-->>AuthoredRoot: dispatch root again
AuthoredRoot->>Kernel: re-drive root
AuthoredRoot-->>Caller: return result or root_lease_lost
Suggested reviewers: Merge Risk: ⚪ Minimal · up to No concrete merge-blocking issue is identified. The lease recovery and attempt-reporting changes appear consistent with their stated contracts; merge after normal checks pass. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change improves recovery without an observed expansion of access or authority. Existing ownership controls remain in place, but repeated interruptions and mixed-version recovery are not fully established. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit checks the ticking lease, Comment |
Bugbot on #593: after a lost root lease the driver waited up to 45s for the kernel's re-dispatch without observing the lifecycle signal, so a Ctrl-C in that window blocked until the timer fired. The wait now rejects at once on abort, and the driver rethrows it as the caller's cancel rather than as root_lease_lost. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ning the run
Customer rw_3a0fcb71 issue 1: an f.agent attempt lost its lease, the kernel
retried it, and the CLI exited `worker wait canceled` / protocol_error with no
run id while attempt 2 went on to succeed -- its output was discarded, and the
failure reported `attempt=2/1`.
Lease timing (kernel + SDK):
- relayflowd sends `lease_ttl_ms` on step.dispatch and step.heartbeat (additive).
- withWorkerLease times the lease on the worker's monotonic clock from that
duration instead of comparing the daemon's absolute deadline with its own
wall clock. A worker 45s ahead of the daemon refused every dispatch as
already expired; 75s behind, it renewed after the daemon swept the lease.
Old daemons keep the old comparison.
Following retries:
- A lost authored ROOT lease is not a failed flow: the root driver waits for
the kernel's re-dispatch and re-drives the body (completed steps replay from
receipts; the in-flight child is re-adopted). It no longer terminalizes the
root as worker_error, which burned every root attempt and made the run
unrecoverable. If no retry arrives it reports `root_lease_lost` (status
running, root run id, exact `flows resume` command).
- classifyOutcome follows a child returned `running` (re-admitted mid-attempt)
and a step in retry `backoff`, instead of "status running without a
classifiable completion" after 2s.
- A worker wait whose lease looks expired only by this process's clock keeps
following the step; it fails only when the deadline stops moving for a
whole lease (never renewed, never swept).
- Cancellation errors carry the abort reason ("worker wait canceled: Agent
lease expired before renewal ...") and withWorkerLease reports a lost lease
as the cause, not the body's generic wait error.
Attempt accounting:
- stepFailureDetails counts attempts the kernel declared dead
(crashed/lease_expired, completed_by kernel) as `unchargedAttempts`, and
the report prints `attempt=2 (iteration 1/1; 1 earlier attempt lost to a
dead worker, not charged)` instead of `attempt=2/1`.
Builds on #590 (terminal-completion read).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bugbot on #593: after a lost root lease the driver waited up to 45s for the kernel's re-dispatch without observing the lifecycle signal, so a Ctrl-C in that window blocked until the timer fired. The wait now rejects at once on abort, and the driver rethrows it as the caller's cancel rather than as root_lease_lost. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
5acbdbc to
ac87f25
Compare
There was a problem hiding this comment.
All reported issues were addressed across 20 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 25f4764. Configure here.
|
Human / Relayflow Lead merge gate is ready for exact head
Per |

Customer rw_3a0fcb71, issue 1. Rebased directly onto
mainatc011e623(v2.0.37); the published head is00a21473.What the customer saw
An
f.agentattempt lost its worker lease. The kernel journaledlease_expiredand retried it, and the CLI exited withworker wait canceled/protocol_errorand no run id, while attempt 2 went on to succeed. Its output was discarded, and the failure report saidattempt=2/1.Root causes
lease_deadline_mson its own clock, butwithWorkerLeasecompared it with the worker'sDate.now()and renewed atremaining/3. If the worker ran 45s ahead, every dispatch was refused asalready_expired. If it ran 75s behind, the first renewal landed after the daemon had swept the lease. A paused process (laptop sleep or VM pause) loses the lease regardless.worker wait canceled.driveRootthen terminalized the root withworker_erroron every retry (all 8 attempts burned), so the run became unrecoverable.run.starton an existing admission key returns the childrunning(mid-retry) or inbackoff.classifyOutcomeonly followedparked, so it reported "status running without a classifiable completion".crashed/lease_expired) is not charged againstmax_iterations(recovery.rs), so the raw attempt number could exceed the budget:attempt=2/1.Fix
lease_ttl_msonstep.dispatchandstep.heartbeat(additive). The SDK times the lease on its monotonic clock from it; older daemons keep the old comparison.root_lease_lostwith statusrunning, the root run id and the exactflows resumecommand.classifyOutcomefollowsrunningoutcomes andbackoffsteps. A worker wait that looks expired only by the local clock keeps following the step; it fails only when the deadline stops moving for a whole lease.withWorkerLeasesurfaces the lease loss as the cause.unchargedAttemptsmakes the report sayattempt=2 (iteration 1/1; 1 earlier attempt lost to a dead worker, not charged).Evidence: failing on the base, passing with this PR, against a real relayflowd and the real
flows run --local-agentCLIHarness: an authored flow with one
f.agentwhose fakeclaudetakes 40s (longer than one 30s lease), run through the built CLI against a real daemon.52137878)protocol_error: lease_conflict, no run id; agent-1 attempts 1–3lease_expired/crashedsuccess, agent-1 1 attemptAgent lease is already expired; root burned 8×worker_error→ run durably failedsuccessprotocol_error, no run id, while agent-1 attempt 2 = successsuccess: rootlease_expired→retry, 2 success; agent-1lease_expired→retry, 2 success(attempt 2's output used)Journal of the passing pause run:
Resuming from the journal after the CLI exits (pause scenario, CLI already exited):
flows resume --local-agent <root>: fails. It reads agent-1's first completion (journal step "agent-1" completed with lease_expired), then burns the root toworker_error ×8, so the run is durably failed and no longer recoverable.flows resume --local-agent <root>: exit 0success. It reuses agent-1's attempt-2 result without re-running it (the root gets a 3rd attempt).Tests
tests/worker-lease-drift-live.test.ts(real daemon): skew ±, andattempt=2/1accounting. Fails on the base.classify-outcome.test.ts: backoff longer than the 2s bound and arunningre-admitted child. The backoff test fails on the base.worker-lease-sweep.test.ts: skewed CLI follows a renewing lease (fails on the base); a never-swept lease still fails at 35s.cli.test.ts: the old "bounds a worker wait" test asserted exit 1 over a mock daemon that then completed success. That is this bug, so it now asserts the step is followed to success.dispatch_carries_the_lease_as_a_duration;cargo test -p relayflowdall green.authored-root > completes the admitted rootflakes on its 30ms fake lease (base 0/5, this branch 3/5 on this machine). About 100 PTY/subprocess/hosted-isolation tests fail identically on the base in this sandbox.🤖 Generated with Claude Code
Note
Medium Risk
Changes lease timing, heartbeat validation, and run-outcome classification across kernel protocol and CLI—security-critical execution path, but behavior is narrowly scoped to skew/retry scenarios with broad test coverage.
Overview
Fixes customer rw_3a0fcb71: worker lease loss and kernel retries no longer cause the CLI to exit with
protocol_error/worker wait canceledwhile the run continues successfully in the journal.Kernel (additive):
step.dispatchandstep.heartbeatnow includelease_ttl_ms(remaining duration on the daemon clock). Heartbeats at or after the daemon-side deadline are rejected so a late renewal cannot revive an expired assignment.Workers:
withWorkerLeasetimes leases onperformance.now()usinglease_ttl_mswhen present, instead of comparing the daemon’slease_deadline_msto the worker’s wall clock. Lease-loss errors win over generic wait-cancel errors from aborted bodies.Authored roots: After a lost root lease, the driver waits for kernel re-dispatch (up to 3 redrives / 45s), replays completed steps, and does not terminalize the root on lease loss. If no retry arrives, it reports
root_lease_lostwithstatus: runningand aflows resumehint—notprotocol_error.CLI classification:
classifyOutcomenow followsrunningoutcomes and steps inbackoff, and worker waits keep polling when the local clock says “expired” but the daemon is still renewing or sweeping. Failure reports addunchargedAttemptssoattempt=2/1is explained when the first attempt died uncharged.Reviewed by Cursor Bugbot for commit 00a2147. Bugbot is set up for automated code reviews on this repo. Configure here.
Agent Relay sessions
claudesessionb714e785· contributor · rangh prcommands · last active 2026-09-30