Skip to content

fix(cgka-engine): classify malformed group messages as terminal stale instead of aborting ingest#893

Open
jgmontoya wants to merge 5 commits into
masterfrom
fix/malformed-445-terminal-stale
Open

fix(cgka-engine): classify malformed group messages as terminal stale instead of aborting ingest#893
jgmontoya wants to merge 5 commits into
masterfrom
fix/malformed-445-terminal-stale

Conversation

@jgmontoya

@jgmontoya jgmontoya commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Open in Stage

Summary by CodeRabbit

  • Bug Fixes
    • Transport “awaiting retry” rows are now retired consistently as failed for malformed payloads and for malformed verdicts produced during snapshot fallback.
    • Prevents overwriting terminal raw states when processing buffered/replayed messages after publish or fork recovery.
    • Malformed group events (including hostile kind-445) no longer block later deliveries or trigger sync-failure telemetry during cold catch-up.
    • Ensures self-evicted rows remain failed after deferred-peel sweep replay.
  • Tests
    • Expanded ingest, buffering/rollback, replay/retirement, fork-recovery, and deferred-peel regression coverage.
    • Added acceptance test for cold catch-up drain resilience to hostile kind-445 inputs.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Changes

Malformed ingest handling

Layer / File(s) Summary
Retryable raw-message retirement
crates/cgka-engine/src/message_processor/store.rs, crates/cgka-engine/src/message_processor/ingest.rs, crates/cgka-engine/src/message_processor/mod.rs
Terminal paths retire deferred or retryable raw rows without overwriting terminal states during replay.
Malformed terminal classification
crates/cgka-engine/src/message_processor/ingest.rs
Direct and snapshot-fallback malformed results become terminal PeelFailed stale outcomes with shared retirement and deduplication handling.
Malformed ingest and drain coverage
crates/cgka-engine/tests/ingest.rs, crates/marmot-app/tests/malformed_445_drain.rs
Tests cover malformed classification, rollback replay, continued delivery, and cold-drain behavior.
Fork replay state preservation
crates/cgka-engine/tests/fork_detection.rs, crates/cgka-engine/tests/deferred_peel_lifecycle.rs
Replay tests verify completed rows become Processed while self-evicted terminal rows remain Failed.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main behavior change: malformed group messages are now treated as terminal stale instead of aborting ingest.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/malformed-445-terminal-stale

Comment @coderabbitai help to get the list of available commands.

@jgmontoya jgmontoya changed the title fix(cgka-engine): classify malformed group messages as terminal stale… fix(cgka-engine): classify malformed group messages as terminal stale instead of aborting ingest Jul 16, 2026
@stage-review

stage-review Bot commented Jul 16, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 5 individual chapters for you:

Title
1 Introduce storage helpers for retryable message rows
2 Classify malformed messages as terminal stale
3 Prevent clobbering terminal states during replay
4 Add regression tests for ingest and replay
5 Verify transport drain resilience to hostile input
Open in Stage

Chapters generated by Stage for commit 28cb0c8 on Jul 17, 2026 4:59pm UTC.

@jgmontoya

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cgka-engine/tests/ingest.rs`:
- Around line 663-670: Update the assertion in the replay test around
alice.publish_failed and alice.ingest to require the specific terminal
IngestOutcome::PeelFailed variant, rather than accepting any
IngestOutcome::Stale result. Preserve the diagnostic message while ensuring
Retryable rows that lead to AlreadySeen cannot satisfy the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31e3d88b-8a5d-41f4-8ba3-76e590089591

📥 Commits

Reviewing files that changed from the base of the PR and between 865d5b2 and ddc50f2.

📒 Files selected for processing (4)
  • crates/cgka-engine/src/message_processor/ingest.rs
  • crates/cgka-engine/src/message_processor/mod.rs
  • crates/cgka-engine/tests/ingest.rs
  • crates/marmot-app/tests/malformed_445_drain.rs

Comment thread crates/cgka-engine/tests/ingest.rs
Base automatically changed from feat/commit-loss-epoch-gap-backfill to master July 17, 2026 02:26
@jgmontoya
jgmontoya force-pushed the fix/malformed-445-terminal-stale branch from ddc50f2 to 6bfeaa2 Compare July 17, 2026 02:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cgka-engine/src/message_processor/ingest.rs`:
- Around line 1596-1607: Move
mark_raw_transport_message_failed_if_awaiting_retry and its durable
state-transition, audit-persistence, and deferred-row-accounting logic from
ingest.rs into the store/dedup layer in message_processor/store.rs. Expose a
store-layer helper with the required inputs, then update ingest callers to
invoke it while keeping ingest focused on peel, classify, and apply.
- Line 601: Update the WrongEpoch handling in the relevant message-processing
function so every terminal outcome retires the raw retry row identified by
raw_msg_id, including IncumbentWins, missing-snapshot fail-closed returns, and
the final AlreadyAtEpoch return. Apply
mark_raw_transport_message_failed_if_awaiting_retry consistently before those
returns, while preserving the existing content-derived msg.id updates and
too-distant-history behavior.
- Around line 383-403: Handle PeelerError::Malformed from
try_peel_group_message_from_available_snapshots at every fallback call site in
do_ingest using the same terminal behavior as the shown direct-path branch:
retire any awaiting retry row via
mark_raw_transport_message_failed_if_awaiting_retry, mark the message ID seen,
and return Stale with reason PeelFailed instead of propagating the error. Add a
regression test covering malformed content discovered only through a retained
snapshot.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d35d2eaf-7398-40a5-8cc6-ad25d4009dba

📥 Commits

Reviewing files that changed from the base of the PR and between ddc50f2 and 6bfeaa2.

📒 Files selected for processing (4)
  • crates/cgka-engine/src/message_processor/ingest.rs
  • crates/cgka-engine/src/message_processor/mod.rs
  • crates/cgka-engine/tests/ingest.rs
  • crates/marmot-app/tests/malformed_445_drain.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/cgka-engine/src/message_processor/mod.rs
  • crates/marmot-app/tests/malformed_445_drain.rs
  • crates/cgka-engine/tests/ingest.rs

Comment thread crates/cgka-engine/src/message_processor/ingest.rs
Comment thread crates/cgka-engine/src/message_processor/ingest.rs
Comment thread crates/cgka-engine/src/message_processor/ingest.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cgka-engine/src/message_processor/mod.rs`:
- Around line 883-897: Update the StaleReason::PeelFailed handling around
ingest_group_message so that when ingestion terminalizes the row as Failed, it
also releases the corresponding in-memory PeelDeferred flood-cap slot and
removes its attempt entry. Keep the cleanup atomic with the terminal state
transition to avoid torn bookkeeping, while preserving retry-state behavior for
rows that remain retryable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7dbc1660-f77e-44e6-b24d-4a5dbc5146bf

📥 Commits

Reviewing files that changed from the base of the PR and between e255d19 and 5ae9a44.

📒 Files selected for processing (4)
  • crates/cgka-engine/src/message_processor/mod.rs
  • crates/cgka-engine/src/message_processor/store.rs
  • crates/cgka-engine/tests/fork_detection.rs
  • crates/cgka-engine/tests/ingest.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/cgka-engine/src/message_processor/store.rs

Comment thread crates/cgka-engine/src/message_processor/mod.rs
@jgmontoya
jgmontoya force-pushed the fix/malformed-445-terminal-stale branch from 0fe8e07 to 3993185 Compare July 17, 2026 16:49
…r module split

mark_raw_transport_message_failed_if_awaiting_retry is durable stored-message
state, so it belongs with store.rs's persistence helpers, not the ingest
peel/classify/apply path. Pure relocation, no behavior change.
…l stale instead of aborting the drain

The two snapshot-fallback call sites propagated a PeelerError::Malformed with
? , aborting the whole transport drain on one garbage event. Give them the
direct peel's terminal handling via a shared malformed_terminal_stale helper.
Defense-in-depth for the public TransportPeeler contract, not a
production-reachable Nostr bug; closes the one-seam guard gap (mdk#707).
…comes

Closes a pre-existing gap in replay_buffered_messages where a raw transport
row buffered Retryable during our own PendingPublish/Merging window was never
retired when re-ingest returned a terminal Ok outcome (Processed, AlreadySeen,
AlreadyAtEpoch/EpochInvalidated, OwnEcho, SelfEvicted): it lingered Retryable
and was re-peeled on every later publish-cycle replay. The Ok(_) arm now
retires the raw wrapper Processed (cap-slot release stays gated on PeelDeferred,
so no mdk#339 accounting impact); UnknownGroup moves to the leave-in-retry arm
since ingest deliberately re-buffers it.

The retirement is guarded on the row's re-read current state so it fires only
while the row is still awaiting retry: a terminal state ingest already committed
to the same row during re-ingest (e.g. SelfEvicted persisting it Failed) is
authoritative and is never clobbered to Processed.
…ering ingest-committed terminal state

Seam parity with replay_buffered_messages (5ae9a44). The deferred-peel
sweep snapshots its PeelDeferred rows once, then re-ingests each via
ingest_group_message. Both terminal-outcome arms
(Ok(Buffered|Processed) and the Ok(Stale{..}) catch-all) retired the raw
wrapper Processed UNCONDITIONALLY. But ingest can commit a terminal state
to the same row during the call: a deferred future-epoch commit that
re-ingests against a now-inactive group hits the !is_active gate, which
persists the row Failed (SelfEvicted, ingest.rs). Relabeling that
evicted-on row Processed swept it back into canonicalization
(openmls_projection / distributed_convergence select on Processed);
a fork-recovery rollback that deletes the row mid-sweep would instead
crash the write with Storage(NotFound).

Both writes now re-read the row's current state through the existing
raw_transport_row_awaiting_retry helper and retire only a state ingest
left awaiting retry, so ingest's terminal verdict is authoritative. The
flood-cap release (note_peel_deferred_row_retired) stays outside the
guard, mirroring 5ae9a44: the row leaves the retry lifecycle regardless
of who terminalized it. The budget-terminal arm (runs before this row's
ingest) and the ForkedEpoch arm (forked_epoch_fail_closed already wrote
the row EpochInvalidated, then returns) are left unchanged.
@jgmontoya
jgmontoya force-pushed the fix/malformed-445-terminal-stale branch from 3993185 to 28cb0c8 Compare July 17, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant