Agent commands wait on observed progress, not the clock (GH-3748, GH-3750) - #3768
Merged
Conversation
…3750) Slow agent work — a Marten projection shard replaying behind a version bump was measured at 27s p50 / 215s max PER SHARD — outran every achievable reply window, and the old protocol treated the window elapsing as failure: the whole chunk was released, the pending-assignment ledger's TTL expired, and agents still mid-start were re-placed onto other nodes (369 of 902 agents started more than once in the GH-3750 field data, every repeat on a different node). Three changes, one protocol: * QueryAgentPresence/AgentPresenceReport: a leader can ask a node which of the named agents are actually running there, answered instantly from the in-memory agent registry. Absence confirms a stop just as presence confirms a start. * Remote StartAgents/StopAgents execute on a background FIFO off the destination's control lane (DeferredAgentCommandRunner). The database control endpoint is strictly serial, so a slow inline batch used to starve every other control message to that node — including the queries above. The typed reply still goes out only at true completion, correlated to the original request, so a pre-upgrade leader's InvokeAsync<T> completes exactly as before. A per-agent stop-revocation sequence preserves the stop-after-start ordering the serial lane used to provide. * The leader races the reply against presence polling (AgentWorkConfirmation): progress within AgentProgressStallTimeout keeps the wait alive, so a slow-but-converging node gets unbounded time while a wedged or mute one costs a bounded wait. The reply window stops doing double duty as a failure detector. Confirmed agents re-arm the pending ledger (ConfirmDispatched) so the one-snapshot gap between confirmation and the assignment row becoming visible can no longer re-decide an agent that just started. New SlowTests harness (slow_starts_outrun_reply_windows): 36 agents at 6/12/18s per start against 42s reply windows converge with every agent started exactly once. Full CoreTests, both existing agent scale harnesses, and the persistence agent suites are green. Closes GH-3750. Closes GH-3748. Part of the GH-3753 campaign. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 1, 2026
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.
Closes #3748. Closes #3750. Part of the #3753 campaign, building on #3757's reply-window mitigation.
The defect pair
Both issues are one protocol flaw seen from two sides: the batched agent command reply window did double duty as a work budget and a failure detector, and no number can be both.
What changed
1. A progress probe:
QueryAgentPresence→AgentPresenceReport. The leader can ask a node which of the named agents are running right now, answered instantly from the in-memory agent registry. Presence confirms a start; absence confirms a stop (an agent the node has never seen is exactly as stopped as one it just stopped).2. Remote
StartAgents/StopAgentsexecute off the control lane (DeferredAgentCommandRunner). The database control endpoint is deliberately serial (MaxDegreeOfParallelism = 1), so a slow inline batch used to monopolize the node's entire control plane — stops queued behind it, and nothing could answer the progress probe. Batches now run on a background FIFO (strictly one at a time, preserving inter-batch ordering and the parallelism cap) and the control lane is free in microseconds. The typed reply still goes out only at true completion, correlated to the original conversation, so a pre-upgrade leader blocked inInvokeAsync<AgentsStarted>completes exactly as before. A per-agent stop-revocation sequence keeps the stop-after-start ordering the serial lane used to guarantee: a stop that lands while its target's start is queued or in flight either suppresses that start or reverts it immediately.3. The leader races the reply against presence polling (
AgentWorkConfirmation). The reply wins whenever it arrives — including from pre-upgrade nodes whose polls simply go unanswered and for whom the old reply window remains the bound (no rolling-upgrade regression in either direction). The polls add the missing piece: observed progress. One more agent confirmed withinDurability.AgentProgressStallTimeout(default 5 min, the daemon side-effect gate's ceiling) keeps the wait alive, so a slow-but-converging node gets unbounded time while a wedged or mute one costs a bounded wait. Every agent stays held — dispatcher in-flight + pending ledger — for as long as the command is waiting, which is what stops the re-placement churn. On resolution, confirmed agents re-arm the ledger (ConfirmDispatched) so the one-snapshot gap before their assignment rows become visible can't re-decide an agent that just started. The reassignment single-copy invariant is unchanged: a start is only cascaded for agents confirmed gone from the source, now by acknowledgement or observation.New knobs:
Durability.AgentProgressPollInterval(10s) andDurability.AgentProgressStallTimeout(5 min).Evidence
slow_starts_outrun_reply_windows: 3 nodes, 36 agents at heterogeneous 6/12/18s per start (36s/72s/108s of work per chunk against 42s reply windows). Converges with every agent started exactly once and asserts the reporter's own churn metric (no agent completes a start on more than one node). Getting this green caught three real bugs in the initial implementation — it is a sensitive guard.agent_assignment_at_scale,agent_reassignment_at_scale), and the persistence agent/control-endpoint suites are green locally; fullwolverine.slnxRelease build is clean.Honest note on the baseline: this miniature also passes on current
main— the GH-3698/#3757 machinery already makes a 3-node/36-agent cluster eventually-convergent, and the field failure needs production scale (thousands of agents, 512 tenant databases, 5-minute gates, rolling pods) to turn "eventually" into "hours". The discriminating evidence for the root fix is the reporters' field data; what this PR removes is the mechanism — the window-as-failure-detector — rather than tuning its numbers.🤖 Generated with Claude Code