Skip to content

fix(OMN-14498): close non-outbox result-applier publish seam at default flag state - #2672

Merged
jonahgabriel merged 4 commits into
devfrom
jonah/omn-14498-result-applier-dlq-seam
Aug 16, 2026
Merged

jonahgabriel merged 4 commits into
devfrom
jonah/omn-14498-result-applier-dlq-seam

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Aug 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes the residual seam adversarial verify proved still open on OMN-14498
after PRs #2436 (sync-publisher DLQ route) and #2604 (DLQ correlation
lineage / unpersisted-DLQ NACK): at the production default
(ONEX_BOUNDARY_DLQ_ENABLED unset), a forced result_applier.apply()
publish failure on a non-outbox (propagate_publish_failures=False,
i.e. non-state_io) contract dispatched through the real
_make_event_bus_callback still returned normally (ACK) with
_publish_raw_to_dlq never awaited — a silent drop
, exactly what the
ticket's acceptance criterion forbids.

Full refutation, including the live counter-probe against the real
production callback at default flag state, is in Linear comment
3c6da9a0-7d29-4bbc-9342-585aea0e8b5d
on OMN-14498.

Root cause

_dispatch_with_bounded_retry's result_applier.apply() except-clause had
two arms:

  • propagate_publish_failures=True (state_io/outbox contracts): wraps in
    BoundaryPublishError, propagates — correctly satisfies "offset not
    committed."
  • propagate_publish_failures=False (the general/non-outbox case): bare
    raise, which rides the generic retry arm and, on exhaustion, falls into
    the flag-gated _route_swallowed_exception. At the default (unset)
    flag state that function finds no DLQ target, logs one line, and
    returns normally — an ACK.

handler_wiring.py:7270 wires propagate_publish_failures=_contract_declares_state_io(contract)
— every non-state_io contract (the overwhelming majority) takes the
silent-swallow arm at default config.

Fix

Mirrors the same unconditional DLQ idiom PR #2436 already uses for the
sync-publisher leg, applied to this seam specifically (not to the whole
flag-gated raw-handler-exception path):

  • New _route_apply_publish_failure helper: on a non-outbox
    result_applier.apply() failure, attempts _publish_raw_to_dlq
    unconditionally — not staged behind ONEX_BOUNDARY_DLQ_ENABLED. The
    dispatch already succeeded here; only the downstream publish of its
    known-good result failed, so this is not the doubtful/unvalidated
    payload the staged rollout exists to hold back.
    • DLQ write persists → returns normally (safe ACK).
    • No DLQ-capable bus, a documented False non-persistence return, or the
      DLQ publish itself raising → raises new BoundaryApplyPublishError.
  • New BoundaryApplyPublishError exception: added to
    _dispatch_with_bounded_retry's non-retryable tuple (the handler already
    ran once; retrying can't fix a downstream publish problem) and given a
    dedicated except clause in callback() so it is never re-absorbed
    by the flag-gated _route_swallowed_exception — it propagates
    unconditionally out of the boundary, withholding the offset.
  • _dispatch_with_bounded_retry now takes the raw message (threaded from
    callback()) so the DLQ write preserves the same ingress fidelity
    _route_swallowed_exception already gets (headers/offset/partition), not
    a reconstructed stand-in.

Untouched (staged-rollout semantics preserved for every other seam): a
raw handler/dispatch exception that never reaches apply() still keeps the
historical default-flag-off swallow — reproved by rerunning
test_boundary_dlq_omn14507.py::TestBoundaryDlqFlagOff unmodified. The
outbox/state_io path (propagate_publish_failures=True) is unaffected —
still propagates the original cause directly, unwrapped.

Seam definition (interacting-work deliverable)

Seam element Value
New exception type BoundaryApplyPublishError(topic: str, correlation_id: object, cause: Exception) — module omnibase_infra.runtime.auto_wiring.handler_wiring
New helper _route_apply_publish_failure(exc, *, event_bus, topic, message, correlation_id) -> None (raises BoundaryApplyPublishError on non-persistence, else returns)
Changed signature _dispatch_with_bounded_retry(envelope, message) — added positional message: object (was envelope only)
DLQ call kwargs (unchanged shape, new failure_type) original_topic, raw_msg=message, error, correlation_id, failure_type="apply_publish_failed", consumer_group="auto-wiring", dlq_topic
Flag dependency None for this seam — ONEX_BOUNDARY_DLQ_ENABLED is not read anywhere in the new code path
Non-retryable tuple addition BoundaryApplyPublishError added alongside BoundaryPublishError in _dispatch_with_bounded_retry's except tuple

RED-before / GREEN-after proof

New file tests/unit/runtime/auto_wiring/test_result_applier_publish_failure_seam_omn14498.py
drives the real _make_event_bus_callback (not a stub), non-outbox
defaults, ONEX_BOUNDARY_DLQ_ENABLED explicitly unset
(monkeypatch.delenv).

RED (against pre-fix dev HEAD a853500ab3):

$ uv run pytest tests/unit/runtime/auto_wiring/test_result_applier_publish_failure_seam_omn14498.py -v
ImportError: cannot import name 'BoundaryApplyPublishError' from
'omnibase_infra.runtime.auto_wiring.handler_wiring'

GREEN (post-fix):

$ env -u ONEX_BOUNDARY_DLQ_ENABLED uv run pytest tests/unit/runtime/auto_wiring/test_result_applier_publish_failure_seam_omn14498.py -v
TestApplyPublishFailureDefaultFlagState::test_apply_publish_failure_routes_to_dlq_at_default_flag_state PASSED
TestApplyPublishFailureDefaultFlagState::test_apply_publish_failure_no_dlq_route_is_never_silently_dropped PASSED
TestApplyPublishFailureDefaultFlagState::test_apply_publish_failure_dlq_write_not_persisted_is_never_acked PASSED
TestApplyPublishFailureDefaultFlagState::test_apply_publish_failure_dlq_publish_itself_raises_is_never_acked PASSED
TestApplyPublishFailureOutboxPathUnchanged::test_outbox_path_still_propagates_original_cause PASSED
5 passed

Regression scope run post-fix:

  • OMN-14498/14507 family (26 tests, including the two existing OMN-14498
    seam files + test_boundary_dlq_omn14507.py unmodified) — 26 passed
  • tests/unit/runtime/auto_wiring/ — 467 passed
  • tests/unit/runtime/ — 5445 passed, 5 skipped

Gates

  • uv run ruff format / ruff check --fix — clean
  • uv run mypy src/omnibase_infra/runtime/auto_wiring/handler_wiring.py --strict — clean
  • uv run mypy tests/unit/runtime/auto_wiring/test_result_applier_publish_failure_seam_omn14498.py --strict — clean
  • pre-commit run --files <changed files> — all hooks passed
  • Pre-push hooks (mypy, architecture-layer validation, governed impacted-test
    selector — escalated to the full local suite for this shared-module diff,
    23173 passed, deploy-scope DoD parity) — all passed

OMN-14498

Evidence-Source: OCC#6266
Evidence-Ticket: OMN-14498
Evidence-Head: 53c05db39267693c8821aa46d67e4f831b33416e

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of publish failures during message processing.
    • Failed messages are routed to the dead-letter queue when available.
    • Processing offsets are not acknowledged if dead-letter persistence or publication fails, preventing message loss.
    • Clear failures are surfaced when dead-letter handling is unavailable or unsuccessful.
    • Existing outbox failure behavior remains unchanged.
    • Improved tenant data migration and row-level access behavior.
  • Tests

    • Added coverage for successful and unsuccessful dead-letter handling scenarios.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 43 minutes

Limit details: You’ve used all 1 included review currently available under your plan. You completed 114 included PR reviews in the past 7 days; at that activity level, included reviews refill at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f8a18a4-86cc-4233-939e-b52204acae5c

📥 Commits

Reviewing files that changed from the base of the PR and between 254c367 and 8d8cf11.

📒 Files selected for processing (3)
  • src/omnibase_infra/runtime/auto_wiring/handler_wiring.py
  • tests/integration/test_state_io_outbox_seam_red.py
  • tests/unit/runtime/auto_wiring/test_result_applier_publish_failure_seam_omn14498.py
📝 Walkthrough

Walkthrough

The runtime now routes non-outbox result-applier publish failures to the DLQ without relying on a flag. It raises when durable DLQ persistence fails, preserves the consume offset, and keeps outbox propagation unchanged. Two migrations update tenant IDs and RLS behavior. CI checkout credentials are persisted.

Changes

Publish Failure DLQ Handling

Layer / File(s) Summary
DLQ routing and boundary error
src/omnibase_infra/protocols/protocol_apply_publish_failure_dlq_publisher.py, src/omnibase_infra/runtime/auto_wiring/handler_wiring.py
The runtime defines the DLQ persistence contract, adds BoundaryApplyPublishError, and handles successful, unavailable, failed, or unsuccessful DLQ persistence.
Callback propagation and classification
src/omnibase_infra/runtime/auto_wiring/handler_wiring.py
The callback passes the original message and correlation ID into bounded dispatch, routes non-outbox failures outside the flag-gated swallow path, and classifies boundary errors as non-retryable.
Publish failure regression coverage
tests/unit/runtime/auto_wiring/test_result_applier_publish_failure_seam_omn14498.py, tests/unit/contracts/test_protocol_ownership.py
Tests cover DLQ success, correlation propagation, failed DLQ persistence, publication errors, unchanged outbox propagation, and protocol ownership.

Database Migration Updates

Layer / File(s) Summary
Capability score tenant UUID conversion
docker/migrations/forward/nodes/node_canary_score_reducer/0003_capability_scores_tenant_id_to_uuid.sql
The migration validates the schema, maps tenant slugs fail-closed, converts tenant_id to UUID, restores tenant isolation policy behavior, and grants dashboard read access.
Node registry RLS adjustment
docker/migrations/forward/nodes/node_projection_registration/0004_node_service_registry_no_force_rls.sql
The migration applies NO FORCE ROW LEVEL SECURITY when the table exists and otherwise emits a notice.
Migration manifest validation
tests/unit/scripts/validation/test_application_migration_manifest.py
The manifest test expects 96 declarations and documents the added migrations.

CI Checkout Credentials

Layer / File(s) Summary
Persisted checkout credentials
.github/workflows/ci.yml
The effect assertion gate checkout persists Git credentials for later history-fetching steps.

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

Sequence Diagram(s)

sequenceDiagram
  participant ResultApplier as result_applier.apply()
  participant EventCallback as event callback
  participant DLQ as DLQ publisher
  participant ConsumeOffset as consume offset
  ResultApplier->>EventCallback: publish exception
  EventCallback->>DLQ: persist original message and failure metadata
  DLQ-->>EventCallback: persistence result
  alt DLQ persistence succeeds
    EventCallback->>ConsumeOffset: acknowledge consumed message
  else DLQ persistence fails
    EventCallback-->>ResultApplier: BoundaryApplyPublishError
    EventCallback->>ConsumeOffset: leave offset unacknowledged
  end
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the fix for non-outbox result-applier publish failures when the default flag state is active.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jonah/omn-14498-result-applier-dlq-seam

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

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

❓ Hostile Reviewer — UNKNOWN

Blocking findings (critical): 0
Total findings: 0
Models succeeded: none


Gate semantics (pilot phase)

Verdict Meaning Blocks merge?
passed No critical findings No
blocked CRITICAL findings found Yes
degraded All models unavailable (infra) No (pilot)

Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8468/OMN-8524)

@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: 2

🤖 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 `@src/omnibase_infra/runtime/auto_wiring/handler_wiring.py`:
- Around line 4560-4561: Propagate the resolved correlation_id from callback()
through _dispatch_with_bounded_retry into _route_apply_publish_failure,
replacing the envelope.correlation_id or uuid4() fallback when constructing the
DLQ record. Add a regression test covering a header-only correlation ID and
assert that the DLQ call receives that resolved value.
- Around line 4331-4385: Refactor _route_apply_publish_failure to remove its
event_bus parameter and all direct access to _publish_raw_to_dlq. Inject and use
a container-provided DLQ protocol service or typed effect executor for
persistence, while preserving the existing unconditional routing and
BoundaryApplyPublishError behavior when persistence is unavailable or fails.
Update callers such as _make_event_bus_callback.callback to pass the new
dependency without exposing the event-bus object.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6fd27bef-e58e-47c6-b205-b7c2c89b976a

📥 Commits

Reviewing files that changed from the base of the PR and between d1c5927 and 923b7db.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • src/omnibase_infra/runtime/auto_wiring/handler_wiring.py
  • tests/unit/runtime/auto_wiring/test_result_applier_publish_failure_seam_omn14498.py

Comment thread src/omnibase_infra/runtime/auto_wiring/handler_wiring.py
Comment on lines +4560 to +4561
message=message,
correlation_id=envelope.correlation_id or uuid4(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate the resolved ingress correlation ID to the DLQ.

Line 4561 uses envelope.correlation_id or uuid4(). When an input has a correlation ID only in transport headers, callback() already resolves that ID at Line 4793, but this route creates a new ID. The DLQ record then loses ingress lineage.

Pass the callback’s resolved correlation_id into _dispatch_with_bounded_retry and then into _route_apply_publish_failure. Add a regression test with a header-only correlation ID and assert that the DLQ call receives it.

🤖 Prompt for 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.

In `@src/omnibase_infra/runtime/auto_wiring/handler_wiring.py` around lines 4560 -
4561, Propagate the resolved correlation_id from callback() through
_dispatch_with_bounded_retry into _route_apply_publish_failure, replacing the
envelope.correlation_id or uuid4() fallback when constructing the DLQ record.
Add a regression test covering a header-only correlation ID and assert that the
DLQ call receives that resolved value.

Source: Coding guidelines

@jonahgabriel

Copy link
Copy Markdown
Collaborator Author

Temporarily closed to relieve CI runner-fleet congestion (merge-sweep 2026-08-08, operator-approved — see ROLLING_WORK_LEDGER [mergesweep-0808-close]). Nothing is lost: branch, commits, and review state remain; this PR will be reopened in batches once the queue drains (reopen order recorded in the ledger).

@jonahgabriel

Copy link
Copy Markdown
Collaborator Author

Reopened and rebased onto dev (past the OMN-15732 migration-vendor deadlock fix, 211e81e) to clear a stale node-migration-sync/OMN-15361 red. This PR's own content is unchanged.

@jonahgabriel
jonahgabriel force-pushed the jonah/omn-14498-result-applier-dlq-seam branch from 2712937 to be65d9c Compare August 8, 2026 18:47
…lt flag state

Adversarial verify (Linear comment 3c6da9a0-7d29-4bbc-9342-585aea0e8b5d)
proved the prior proof-rerun PASS was vacuous: the two existing OMN-14498
seam tests either drive the unrelated sync-publisher leg or force
ONEX_BOUNDARY_DLQ_ENABLED=1, a configuration the runtime does not ship
with. At the real production default (flag unset), a forced
result_applier.apply() publish failure on a non-outbox
(propagate_publish_failures=False) contract still returned normally from
_make_event_bus_callback -- an ACK -- with _publish_raw_to_dlq never
awaited: a silent drop.

Fix: _dispatch_with_bounded_retry's apply() except-clause now routes a
non-outbox publish failure through a new _route_apply_publish_failure
helper that attempts the DLQ write UNCONDITIONALLY (mirroring the
unconditional idiom #2436 already uses for the sync-publisher leg,
instead of staging it behind ONEX_BOUNDARY_DLQ_ENABLED) -- the dispatch
already succeeded here, only the downstream publish of its known-good
result failed, so this is not the doubtful/unvalidated payload the
staged rollout exists to hold back. If the DLQ write persists, the
callback returns normally (safe ACK). If it cannot persist (no
DLQ-capable bus, a False non-persistence return, or the DLQ publish
itself raising), a new BoundaryApplyPublishError propagates out of the
callback unconditionally -- never re-absorbed by the flag-gated
_route_swallowed_exception -- so the offset is withheld instead.

Untouched: a raw handler/dispatch exception that never reaches apply()
still keeps the historical staged-rollout swallow at the default flag
state (test_boundary_dlq_omn14507.py::TestBoundaryDlqFlagOff, rerun
green, unmodified). The outbox/state_io path
(propagate_publish_failures=True) is unaffected -- still propagates the
original cause directly.

RED-before/GREEN-after: the new test file failed on collection
(ImportError: BoundaryApplyPublishError) against unfixed dev HEAD,
proving the fix was previously absent; all 5 new tests plus the full
26-test OMN-14498/14507 family plus the 467-test auto_wiring directory
plus the 5445-test unit/runtime directory pass post-fix.

OMN-14498
[mergesweep-0809-infraunblock] OCC#6266 merged 2026-08-09T18:10:57Z (supersedes
the closed regenerate-class OCC#6181). This gate polls a frozen head at trigger
time; retriggering to pick up the new companion state.
`test_non_outbox_boundary_still_swallows_unchanged` asserted that a
non-outbox contract's `result_applier.apply()` publish failure was STILL
log-and-discarded (ACK) at the default `ONEX_BOUNDARY_DLQ_ENABLED` state.
That was never a claim that swallowing is correct: the test's own docstring
scoped it as "P3b must not broaden the un-swallow to every auto-wired
contract (the platform-wide hole is OMN-14498/OMN-14507's, not this
ticket's)" -- a deliberate scope fence naming THIS ticket as the owner of
closing the hole.

This is that ticket, and the hole is closed, so the fence is retired rather
than kept green. Replaced with two tests that pin the new contract and
preserve the fence's surviving invariant:

- `test_non_outbox_boundary_nacks_when_no_durable_dlq_route`: with no
  DLQ-capable bus, the boundary must raise `BoundaryApplyPublishError`
  (offset withheld) instead of returning normally. Verified RED against the
  pre-fix implementation, which returns None -- the silent drop itself.
- `test_non_outbox_boundary_acks_when_dlq_durably_captures`: when the
  unconditional DLQ route durably takes the record, a non-outbox contract
  still ACKs. This is the fence's real invariant -- "do not broaden the
  un-swallow into blanket propagation" -- restated for the closed seam. The
  discriminator moved from "outbox vs non-outbox" to "was the record durably
  captured"; nothing is silently dropped in either arm. Also verified RED
  pre-fix (zero DLQ writes attempted).

No production code changed by this commit.
@jonahgabriel
jonahgabriel force-pushed the jonah/omn-14498-result-applier-dlq-seam branch from 8b2f66c to 8d8cf11 Compare August 16, 2026 11:24
@jonahgabriel
jonahgabriel merged commit 6fdbba6 into dev Aug 16, 2026
178 of 190 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-14498-result-applier-dlq-seam branch August 16, 2026 14:26
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