fix: clear exec_and_wait's spawn lock between logical invocations - #2239
Open
keepkeen wants to merge 1 commit into
Open
fix: clear exec_and_wait's spawn lock between logical invocations#2239keepkeen wants to merge 1 commit into
keepkeen wants to merge 1 commit into
Conversation
The detached spawn in `exec_and_wait` is guarded by
`mkdir {lock_dir} 2>/dev/null || exit 0` so a transport-level retry of the
spawn RPC (a severed response replayed by `E2BSandbox._rpc_retry`) cannot
double-execute the command. But the lock dir was never removed, and the
stale-marker cleanup (`rm -f {out_file} {done_file}`) sat *behind* the guard.
So a second call with the same tag hit the guard, ran nothing, and
`_await_done_marker` immediately read the previous run's exit-code marker.
Concrete victim: `harness.common.install_npm_cli` retries a failed npm
install three times with `tag="harness-npm-install"`. Attempts 2 and 3
executed nothing and returned attempt 1's exit code and log verbatim — the
"failed after 3 attempts" error really meant "failed after 1".
Fix: clear the per-invocation state (`rm -rf lock_dir; rm -f out done`) in
its own idempotent RPC *before* the guarded spawn. The guard still dedupes
replays of the spawn RPC itself; it just no longer outlives the invocation.
Also declares `idempotent` on the `Sandbox` Protocol's `exec` — both
`exec_and_wait` call sites pass it, so a third-party sandbox implementing
the previous documented signature would raise TypeError.
Adds tests/test_agent/test_sandbox_exec_and_wait.py (wired into the agent-test
CI job) with a fake that interprets the actual shell commands: a same-tag
reinvocation must really re-run, and a replayed spawn RPC must stay deduped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
exec_and_wait's retry protection makes retries impossible: a second invocation with the sametagexecutes nothing and silently returns the first invocation's exit code and log.Root cause
The detached spawn is guarded so that a transport-level replay of the spawn RPC can't double-execute:
Two problems compose:
lock_diris never removed, so the guard fires for every later logical invocation of the same tag, not just for transport replays of the same spawn..donefile is still there —_await_done_markerreturns immediately with the previous run's exit code, and the.outtail is the previous run's log.Concrete victim
harness.common.install_npm_cliretries a failed npm install three times by design (NPM_INSTALL_RETRIES = 3, "Detached install with a few in-place retries for transient disk flakes"), all withtag="harness-npm-install". Attempts 2 and 3 execute nothing and replay attempt 1's result — the backoff sleeps are the only thing that actually runs.RuntimeError("npm install failed after 3 attempts")really means "after 1 attempt", and a transient flake that would have succeeded on retry becomes a hard failure.Fix
Clear the per-invocation state in its own idempotent RPC before the guarded spawn:
The guard's actual purpose is preserved: a severed-response replay of the spawn RPC still hits
mkdirand exits without double-executing. It just no longer outlives the invocation. (Callers must not overlap two same-tag calls — that was already true, and is now documented at the call site.)Also fixes the adjacent Protocol mismatch:
Sandbox.execdidn't declareidempotent, yet bothexec_and_waitRPCs pass it — a third-party sandbox implementing the documented signature would raiseTypeError. (tests/test_agent/_fakes.pyalready carried the parameter.)Test
New
tests/test_agent/test_sandbox_exec_and_wait.py, wired into theagent-testCI job. The fake sandbox interprets the actual shell stringsexec_and_waitissues (mkdir-guard short-circuit, rm cleanup, setsid launch, marker polls), so the tests pin semantics rather than the exact command layout:mainwithassert 1 == 2(second invocation never spawned) and returns attempt 1's stale(1, "attempt-1 failed");The full
tests/test_agent/suite passes (64 passed, 1 pre-existing skip).