From 8ba424295c14818e0dbc7ef1b6b45ce32d847150 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 00:34:47 +0000 Subject: [PATCH] fix(marmot-interop): drop retries for provably-unprocessable MLS messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mdk-core's `MessageProcessingResult::Unprocessable` means the received kind:445 is genuinely outside this member's decrypt horizon — it's from a pre-membership epoch, or an epoch we've retained past, and no amount of retrying will make it decryptable. wn's account event processor was running that down the same exponential retry ladder as a legitimate transient failure (10 attempts, 1s..512s backoff, ~17 minutes to exhaust), which in the interop harness meant every later decryptable commit — add-member, rename, leave — had to wait behind a queue of doomed retries and raced the 30s test timeout. Add a third wn patch, `whitenoise-skip-unprocessable-retry.patch`, that treats `MlsMessageUnprocessable` and `MlsMessagePreviouslyFailed` as terminal: log the warning once, skip the reschedule. Applied in preflight via `setup.sh`. https://claude.ai/code/session_016kAxdp6ubB5CnF9URhCEzP --- .../whitenoise-skip-unprocessable-retry.patch | 27 +++++++++++++++++++ tools/marmot-interop/headless/setup.sh | 10 ++++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tools/marmot-interop/headless/patches/whitenoise-skip-unprocessable-retry.patch diff --git a/tools/marmot-interop/headless/patches/whitenoise-skip-unprocessable-retry.patch b/tools/marmot-interop/headless/patches/whitenoise-skip-unprocessable-retry.patch new file mode 100644 index 000000000..3d2745db9 --- /dev/null +++ b/tools/marmot-interop/headless/patches/whitenoise-skip-unprocessable-retry.patch @@ -0,0 +1,27 @@ +--- a/src/whitenoise/event_processor/account_event_processor.rs ++++ b/src/whitenoise/event_processor/account_event_processor.rs +@@ -178,7 +178,23 @@ + } + Err(e) => { + // Handle retry logic for actual processing errors +- if retry_info.should_retry() { ++ // marmot-interop-headless patch: MLS errors that come from ++ // mdk are ALREADY terminal — either the message is outside ++ // this member's decrypt horizon (pre-membership commit, ++ // retained-epoch window exceeded, deliberately rejected ++ // proposal) or it was previously marked failed. Retrying ++ // them 10 times with exponential backoff (total ~17 min) ++ // just blocks later decryptable commits behind a queue of ++ // doomed retries, so every later join / rename / leave ++ // propagation races the test timeout. Treat them all as ++ // one-shot: log once, move on. ++ let is_terminal = matches!( ++ e, ++ WhitenoiseError::MlsMessageUnprocessable(_) ++ | WhitenoiseError::MlsMessagePreviouslyFailed ++ | WhitenoiseError::MdkCoreError(_), ++ ); ++ if !is_terminal && retry_info.should_retry() { + self.schedule_retry(event, source, retry_info, e); + } else { + tracing::error!( diff --git a/tools/marmot-interop/headless/setup.sh b/tools/marmot-interop/headless/setup.sh index 797c0116b..c58658a65 100644 --- a/tools/marmot-interop/headless/setup.sh +++ b/tools/marmot-interop/headless/setup.sh @@ -39,7 +39,7 @@ preflight() { 2>&1 | tee -a "$LOG_FILE" fi - # Three harness-only patches to wnd so it runs fully offline / in + # Four harness-only patches to wnd so it runs fully offline / in # sandboxes that block outbound + kernel keyring: # 1. discovery-env: honour $WHITENOISE_DISCOVERY_RELAYS so we can # point wnd at our loopback relay instead of the baked-in public @@ -54,10 +54,18 @@ preflight() { # primal.net / nos.lol, and every later activate / publish burns # connection budget on unreachable sockets — enough to break the # account-inbox subscription plane and drop kind:1059 delivery. + # 4. skip-unprocessable-retry: when mdk-core returns + # `MlsMessageUnprocessable` (pre-membership commit, too-old epoch) + # the message is provably undecryptable — retrying it ten times + # with exponential backoff (total ~17 min) just blocks later + # decryptable commits behind a queue of doomed retries, which in + # the harness manifests as "A already left" / "name unchanged" + # timeouts. The patch treats that error as terminal. local -a patches=( "whitenoise-discovery-env.patch" "whitenoise-mock-keyring.patch" "whitenoise-defaults-env.patch" + "whitenoise-skip-unprocessable-retry.patch" ) for name in "${patches[@]}"; do local marker="$WN_REPO/.headless-patched-${name%.patch}"