Skip to content

Agent commands wait on observed progress, not the clock (GH-3748, GH-3750) - #3768

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-3748-3750/agent-ack-decoupling
Aug 1, 2026
Merged

Agent commands wait on observed progress, not the clock (GH-3748, GH-3750)#3768
jeremydmiller merged 1 commit into
mainfrom
gh-3748-3750/agent-ack-decoupling

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

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: QueryAgentPresenceAgentPresenceReport. 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/StopAgents execute 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 in InvokeAsync<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 within Durability.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) and Durability.AgentProgressStallTimeout (5 min).

Evidence

  • New SlowTests harness 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.
  • 11 new unit tests over the confirmation race (progress resets the stall clock, mute-poll fallback to the legacy window, gone-mode, partial replies, cancellation) plus presence-query serialization/round-trip coverage.
  • Full CoreTests (2192), both existing agent scale harnesses (agent_assignment_at_scale, agent_reassignment_at_scale), and the persistence agent/control-endpoint suites are green locally; full wolverine.slnx Release 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

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant