Fold GatesFailed auto-retry into the block subscriber's shared budget

Code review of 1185 (merge 0f4b0c95) found the retry subscriber's central
invariant did not hold: the MergeFailure->Merge bounce caused by its own
retry reset both its attempt counter and the block subscriber's counter,
so the shared merge_failure_block_threshold budget was unreachable and a
deterministic gates failure retried forever.

One subscriber now owns one counter driving both policies:

- Counter survives PipelineEvent::MergeRetryStarted bounces (finding 1);
  a third consecutive failure blocks even with retries in between.
- Mixed failure kinds share the single budget (finding 5).
- Retries respect recovery: no counting or scheduling while a mergemaster
  is active, and perform_auto_retry re-checks before firing (finding 2).
- perform_auto_retry applies the same eligibility gates as
  assign_merge_stage (review hold, frozen, blocked, unmet deps) so freeze
  now stops a retry loop (finding 4).
- Per-story scheduling generations invalidate stale sleeping timers
  (finding 6).
- One-shot startup scan schedules a catch-up retry for stories already
  parked in GatesFailed, so restarts no longer strand them (finding 3);
  kept out of the periodic reconciler to avoid re-retrying exhausted
  stories every tick.
- Chat is notified only after the merge actually starts; a failed trigger
  logs instead of claiming a retry ran (finding 7).
- Config reads moved onto spawn_blocking (finding 8, bug 1170 class).

Deletes merge_failure_retry_subscriber.rs; notification plumbing
(WatcherEvent::MergeAutoRetry et al) is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fHdm92yjvguPi2LiXfLB9
This commit is contained in:
Timmy
2026-07-17 13:36:45 +01:00
co-authored by Claude Fable 5
parent c717ae7041
commit 3ed3fdd6b0
4 changed files with 609 additions and 894 deletions
+9 -17
View File
@@ -89,24 +89,17 @@ pub(crate) fn spawn_event_bridges(
root.clone(),
);
// Consecutive-failure auto-block subscriber: blocks stories after N
// consecutive MergeFailure transitions (story 1018). Bug 1025: takes
// the agent pool so it can gate the counter on mergemaster presence —
// failures during active recovery iteration do not count toward block.
// Consecutive-failure budget subscriber: auto-retries GatesFailed
// failures below merge_failure_block_threshold (story 1185) and blocks
// stories at it (story 1018), with one shared counter. Bug 1025:
// takes the agent pool so it can gate both policies on mergemaster
// presence — failures during active recovery iteration neither count
// nor retry.
crate::agents::pool::auto_assign::spawn_merge_failure_block_subscriber(
Arc::clone(&agents),
root.clone(),
);
// GatesFailed auto-retry subscriber: re-triggers the server-side merge
// after a delay for GatesFailed failures, bounded by the same
// merge_failure_block_threshold budget the auto-block subscriber above
// uses (story 1185).
crate::agents::pool::auto_assign::spawn_merge_failure_retry_subscriber(
Arc::clone(&agents),
root.clone(),
);
// Content-store GC subscriber: purges all ContentKey::* entries for a
// story when it reaches a terminal stage, preventing zombie entries from
// accumulating in the process heap (story 996).
@@ -548,12 +541,11 @@ pub(crate) async fn run_reconcile_pass(
// Merge-failure: spawn mergemaster for ConflictDetected stories with no active agent.
crate::agents::pool::auto_assign::reconcile_merge_failure(agents, root).await;
// Merge-block: no-op (in-memory counter cannot be reconstructed from CRDT).
// Merge-block: no-op for the periodic pass (in-memory counter cannot be
// reconstructed from CRDT); stranded-GatesFailed catch-up runs once at
// subscriber startup instead.
crate::agents::pool::auto_assign::reconcile_merge_failure_block();
// Merge-retry: no-op (in-memory attempt counter cannot be reconstructed from CRDT).
crate::agents::pool::auto_assign::reconcile_merge_failure_retry();
// Audit-log: no-op (historical replay would produce misleading entries).
crate::pipeline_state::reconcile_audit_log();
}