chat: close wakeFoldedRun concurrent-wake race via lifecycle.ensureAwake - #492
Merged
Merged
Conversation
sendFoldedMailWithReclaimRetry's reclaim path calls wakeByAddressBounded directly, bypassing lifecycle.ensureAwake's per-address dedup even when a lifecycle is configured. Drives a sendMail call whose first delivery attempt fails "agent is unreachable" (forcing the reclaim retry), gates its redeploy open, and fires a concurrent ensureAwake call for the same address -- asserting only one wakeFoldedRun delete+redeploy cycle runs, not two racing ones. Confirmed the test fails (deployCallCount reaches 3) against the unfixed platform-adapter.ts.
wakeFoldedRun clears a run's session_asset rows and redeploys with no lock spanning the two steps, so two concurrent calls for the same instance race on the same primary key and git ref. sendFoldedMailWithReclaimRetry's reclaim path called wakeByAddress directly, bypassing lifecycle.ensureAwake's per-address pendingWakes coalescing even when a lifecycle was configured -- the one caller that could still race a lifecycle-driven wake for the same address. wakeByAddressBounded now routes through lifecycle.ensureAwake whenever a lifecycle is configured, converging every wake path in this adapter (sendMail, the exported ensureAwake hook, and the reclaim retry) onto the one coalescing map @corbits/agent-lifecycle already owns, rather than adding a second one here. reconcileDriftedRun runs unconditionally afterward, matching the CL-6588 pattern already used at the other two call sites, since lifecycle.ensureAwake no-ops on an address that is already routable. Only the no-lifecycle fallback still calls wakeByAddress directly, unchanged.
TheGreatAxios
force-pushed
the
cl-7214-serialize-wakefoldedrun
branch
from
August 30, 2026 21:29
9e613c8 to
840950d
Compare
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.
Summary
wakeFoldedRun(packages/folded-runs) clears a run'ssession_assetmanifest rows and then redeploys, with no lock spanning the two steps — two concurrent calls for the same instance race on the same primary key and the same git ref.@corbits/agent-lifecycle'sensureAwakealready coalesces concurrent wakes onto one in-flight promise per address (and, as of the stacked CL-7217 PR below this one, keeps that dedup alive until the real wake settles, not just until a caller's timeout fires). Butpackages/chat/src/platform-adapter.ts'swakeByAddressBoundedbypassed that coalescing for one caller:sendFoldedMailWithReclaimRetry's reclaim-retry loop calledwakeByAddressdirectly, with its own ad hoc timeout, never touchinglifecycle'spendingWakesmap — even thoughlifecycleis always configured in production (apps/hub/src/index.ts).Fix
wakeByAddressBoundednow routes throughlifecycle.ensureAwake(address)whenever alifecycleis configured, converging the reclaim retry onto the exact same coalescing mapsendMail's lifecycle branch and the exportedensureAwakehook already use — no new lock or map added anywhere inpackages/chat. Only the no-lifecyclefallback still callswakeByAddress+withTimeoutdirectly, unchanged.Confirmed before implementing that
wakeFoldedRunhas exactly one real caller in the repo (wakeByAddress, in this file), and that every wake entry point here resolves the same live address before touching wake logic — so address-keyed coalescing inagent-lifecyclecloses the same race a per-instanceIdlock infolded-runswould, without a second primitive.Known, accepted side effect: the reclaim retry now calls
reconcileDriftedRunafter every wake attempt (up to ~5 retries) — an extra DB round-trip per retry that didn't happen before, mirroring the CL-6588 pattern already used at the other two call sites (lifecycle.ensureAwakeno-ops when already routable, so a staleness check needs to run unconditionally alongside it). Harmless, but flagging explicitly so it doesn't read as a regression in a wake-storm trace.Test
New test in
packages/chat/test/platform-adapter.test.ts("wakeByAddressBounded reclaim-retry coalescing"): drives asendMailcall whose first delivery attempt fails "agent is unreachable" (forcing the reclaim retry), gates the resulting redeploy open, and fires a concurrentensureAwakecall for the same address while it's in flight. Asserts only onesession_assetdelete+redeploy cycle runs, not two racing ones. Manually confirmed this test fails (a third, racing deploy) against the unfixed adapter.Stack
Stacked on
cl-7217-agent-lifecycle-wake-timeout(#491) — this PR's fix depends on that one's coalescing primitive being correct. Merge #491 first.Review
Closes CL-7214.