Skip to content

fix(OMN-16798): a record already on a dead-letter topic must not be re-DLQ'd onto itself - #2951

Merged
github-actions[bot] merged 1 commit into
devfrom
jonah/omn-16798-dlq-self-reference
Aug 28, 2026
Merged

github-actions[bot] merged 1 commit into
devfrom
jonah/omn-16798-dlq-self-reference

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator

OMN-16798 follow-up — the totality guards amplified themselves on a dead-letter topic

omnibase_infra#2949 (merged 35d97fc5, deployed to the .201 dev lane at 23:35:30Z, receipt 20260827T233530Z-35d97fc51650.json, health-gate PASS) fixed both OMN-16798 defects, and the chain is alive: the first post-deploy probe returned a terminal in 2.4 s instead of timing out, routing-decision.v1 moved off its flat HWM of 59 for the first time, and inference was reached.

The 13-class matrix immediately afterwards surfaced a defect that PR introduced, and this fixes it rather than carrying it.

The defect

Both OMN-16798 totality guards answer an undeliverable record by routing it to the topic's DLQ. On a record consumed from a onex.dlq.* topic that answer is circular: get_dlq_topic_for_original resolves a dlq topic back to itself, so the guard republished the record onto the very topic it had just read it from, and the subscriber read it again.

Measured on the dev lane 2026-08-27T23:49-23:51Z. node_dlq_replay_effect and node_ledger_projection_compute both consume onex.dlq.omnibase-infra.commands.v1:

[ERROR] handler_wiring: Auto-wiring callback error:
  topic=onex.dlq.omnibase-infra.commands.v1
  error_type=HandlerDispatchFailureError
  error=... matched no dispatcher in this contract's scope
        (failure_class=no_dispatcher,
         derived_dlq_topic=onex.dlq.omnibase-infra.omnibase-infra.v1) ...
[ERROR] handler_wiring: metric_name=boundary_swallow_prevented dlq_routed=true

24 of the last 38 records on that topic carry original_topic: onex.dlq.omnibase-infra.commands.v1 and error_type: HandlerDispatchFailureError — records the guard itself authored. The run converged (HWM stable at 188 across a 30 s re-probe, both consumer groups Stable at TOTAL-LAG 0, zero no_dispatcher lines in the following 2 minutes, and onex.dlq.omnibase-infra.omnibase-infra.v1 still at HWM 0), but convergence there was luck, not mechanism, and a self-referential DLQ write is not something to leave standing on that basis.

The fix

_is_dead_letter_source_topic(topic) — a record already sitting on a dead-letter sink is already durably captured, which is precisely what the guards exist to secure. AC4's requirement — dispatch, DLQ, or refuse with evidence — is satisfied by that durable record plus a loud structured log (metric_name=boundary_dead_letter_unroutable dlq_routed=false reason=already_on_dead_letter_topic). Republishing adds no evidence and risks amplification, so both guards log and stop.

The amplification was measured on the NO_DISPATCHER leg only. The undeliverable-output leg is guarded by symmetry — the loop mechanism is identical — and the comment says so rather than implying it was observed.

RED before GREEN

Two tests added to tests/unit/runtime/auto_wiring/test_omn16798_boundary_delivery_totality.py (11 in the file). With only the guard reverted (the test file left in place — a surgical revert):

FAILED TestNoDispatcherIsNeverSilent::test_a_record_already_on_a_dlq_topic_is_not_re_dlqd
  AssertionError: an unroutable record consumed FROM a dead-letter topic was
  republished onto that same topic — the guard amplifying its own output
  instead of recording it
ERROR handler_wiring: metric_name=boundary_swallow_prevented dlq_routed=true
  topic=onex.dlq.omnibase-infra.commands.v1
FAILED TestNoDispatcherIsNeverSilent::test_dead_letter_source_topics_are_recognized
2 failed, 9 passed

The first failure emits the live log line verbatim. With the guard restored: 11 passed.

dod_evidence

  • RED→GREEN above, reproducing the live dlq_routed=true on a dead-letter source topic.
  • tests/unit/runtime + tests/unit/topology: 5692 passed, 5 skipped.
  • Governed pre-push selector escalated to the full suite and ran it: 24248 passed, 45 skipped, 0 failed in 662s (impacted tests passed; allowing push). No PREPUSH_FULL_SUITE, no ENABLE_SMART_TESTS=off, no override grant.
  • ruff format + ruff check clean; mypy --strict: Success: no issues found in 2858 source files; pre-commit run --all-files clean.

What #2949 proved live, for the record

probe before after
POST :8085/skill terminal dispatch_timeout after 180 s terminal in 2.4 s
onex.evt.omnibase-infra.routing-decision.v1 flat at 59 across two deploys 59 → 60 on one probe; +44 across the 13-class matrix
onex.cmd.omnibase-infra.delegation-inference-request.v1 flat at 35 35 → 36; +32 across the matrix
terminal status for a resolved routing status=timeout status=failed, terminal_failure_cause=provider_quota_exhausted — a real attributed cause
Auto-wiring result applier registered from published_events (contract=node_delegation_routing_reducer …) absent present at boot

Dev lane only. stability-test, judge and prod were not touched.

Evidence-Ticket: OMN-16798
Evidence-Source: OCC#7347

…e-DLQ'd onto itself

Self-inflicted, found by the first post-deploy matrix run and fixed rather than
carried. The OMN-16798 totality guards answer an undeliverable record by routing
it to the topic's DLQ. On a record CONSUMED FROM a dlq topic that answer is
circular: get_dlq_topic_for_original resolves a onex.dlq.* topic back to itself,
so the guard republished the record onto the very topic it had just read it from.

Measured on the .201 dev lane 2026-08-27T23:49-23:51Z: node_dlq_replay_effect and
node_ledger_projection_compute both consume onex.dlq.omnibase-infra.commands.v1;
a record neither could route was re-DLQ'd onto that same topic, and 24 of the last
38 records on it carried original_topic: onex.dlq.omnibase-infra.commands.v1 --
records the guard itself had authored. It converged that run (HWM stable at 188,
both groups Stable at lag 0), but convergence was luck, not mechanism.

A record already on a dead-letter sink is ALREADY durably captured, which is what
the guards exist to secure. AC4 -- dispatch, DLQ, or refuse with evidence -- is
satisfied by that durable record plus a loud structured log, so both guards now
log and stop instead of republishing. The amplification was measured on the
NO_DISPATCHER leg; the undeliverable-output leg is guarded by symmetry, and the
comment says so rather than implying it was observed.

Evidence-Ticket: OMN-16798
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 31 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 131 included PR review attempts over the past 7 days set your current allowance 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).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab4458f6-dca3-4300-a33e-7e95ba6e77e5

📥 Commits

Reviewing files that changed from the base of the PR and between 35d97fc and 43c4139.

📒 Files selected for processing (2)
  • src/omnibase_infra/runtime/auto_wiring/handler_wiring.py
  • tests/unit/runtime/auto_wiring/test_omn16798_boundary_delivery_totality.py

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

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Hostile Reviewer — DEGRADED (informational)

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

Note: All reviewer models failed or were unavailable. Degraded results are informational during the pilot phase (OMN-8468/OMN-8524) and do not block merge. Error: all review endpoints [192.168.86.201:8000 192.168.86.201:8000 ] unreachable — preflight short-circuit (no models available)


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 — multi-model adversarial review (OMN-8468/OMN-8524)

jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Aug 28, 2026
…ibase_infra#2951 (#7347)

* evidence(OMN-16798): author OCC companion for OmniNode-ai/omnibase_infra#2951

OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head 43c4139b029ef90f337b1a75e4adc4cadb21de98.

* evidence(OMN-16798): self-bind OCC#7347 + rebind contract_sha256

---------

Co-authored-by: omnimarket-bot <bot@omninode.ai>
@github-actions
github-actions Bot merged commit 4529c34 into dev Aug 28, 2026
191 of 198 checks passed
@github-actions
github-actions Bot deleted the jonah/omn-16798-dlq-self-reference branch August 28, 2026 00:49
github-actions Bot pushed a commit that referenced this pull request Aug 28, 2026
…chain (#2953)

OMN-16690: node_hook_event_capture/contract.yaml declared access='write' while
its handler called db.query(). The runtime read seam refused, the projection arm
swallowed the PermissionError and DLQ'd the event, callers kept seeing 202, and
the quarantine sink reached HWM 8,878,932. It ran about a week behind green CI.

The platform testing inventory names why nothing caught it (section 7.1): the
golden-chain DB double carries NO capability enforcement, so a contract/handler
capability mismatch is invisible to all 194 golden chains BY CONSTRUCTION. That
is equally true in THIS repo --
tests/integration/runtime/test_projection_handler_db_injection_integration.py
injects a FakeDb through _build_projection_db_adapter whose upsert/query do no
access checking, so it is green for every declared access value.

omnimarket#2164 closed the statically-visible half (a handler that TEXTUALLY
calls db.query() must declare read_write). It cannot see a read reached through
a helper, a base class or an adapter, and nothing anywhere proved the RUNTIME
refusal is still wired or that a mismatch turns a whole CHAIN red.

Where the seam boundary is drawn
--------------------------------
_assert_read_declared / _assert_write_declared both fire BEFORE the adapter
touches a connection, so the whole capability decision is reachable with zero DB
contact. Real here: _prepare_handler_wiring arm selection,
_make_projection_dispatch_callback, _build_projection_db_adapter,
ProjectionDatabaseOperations domain routing, and ProjectionTableOperation's
assertions and SQL construction. Substituted: the psycopg2 DRIVER only, via
sys.modules -- the technique test_sync_psycopg2_adapter_preserves_text_array_lists
already uses here. Substituting the adapter OBJECT is the fidelity gap being
closed; do not "simplify" this file back into that.

The matrix, both directions (a one-sided fixture can pass by accident):

  read_write declared + query  -> terminal emitted, SQL executed
  read_write declared + upsert -> terminal emitted, SQL executed
  write declared + query       -> REFUSED (the OMN-16690 shape)
  read declared + upsert       -> REFUSED (mirror image)

Only `access` varies across the four rows. Same handler, same wiring, same wire
bytes, same table -- so any divergence is attributable to the capability seam and
to nothing else.

Each refused row asserts THREE separate facts, because any one alone is weak:
no terminal event (the chain does not report success -- the half the live outage
got wrong at the HTTP boundary); the event reaches the contract-declared DLQ
carrying the PermissionError (bus-observable, not log-only -- log-only detection
is what let the live defect run a week); and ZERO SQL was constructed (the
refusal precedes statement construction, so it is a gate and not a late check).

Two fixture facts that are load-bearing, both found by measurement
-----------------------------------------------------------------
1. The table is projection_watermarks, a REAL table from the shipped local
   topology. A fabricated name cannot reach the capability seam at all:
   _require_projection_binding_privileges rejects it three layers earlier, and
   the only sanctioned grant-synthesis helper refuses for already-shipped tables.
   A fabricated row would have proven nothing about access.
2. The contract must set terminal_event AND list it in publish_topics
   (handler_wiring.py:8486-8492) or the arm emits no terminal -- which would make
   a positive row's observation identical to a refused row's, i.e. vacuous.

RED-first proof, and what it corrected
--------------------------------------
Stubbing BOTH production assertions to `lambda self: None` does NOT make the
PermissionError disappear -- the chain then fails a SECOND, independent refusal
("Projection operation has no declared workload binding"), because a
write-declared table has no read binding to resolve either.

So an assertion of the form `"PermissionError" in dlq_text` STAYS GREEN with the
capability seam switched off and would gate nothing. Asserting the seam's OWN
WORDING is what makes these rows non-vacuous. Measured, then recorded at the
assertion:

  neutered  -> 2 failed, 3 passed  (both refused rows RED, on that line only)
  restored  -> 9 passed            (whole tests/integration/chains/ suite)

AC3 also anchors by importing ProjectionTableOperation and asserting both refusal
methods exist on the production class. omnimarket's
node_projection_live_events/tests/test_projection_live_events.py:38 re-implements
`access not in {"read","read_write"}` and asserts its own copy -- it was green
throughout the outage, because a mirror of a rule cannot tell you the rule is
still wired.

Enforcement, not detection (rule 5): no workflow edit. The Event Chain Gate job
already runs `uv run pytest tests/integration/chains/` wholesale and is
registered in ci_summary_gate.py::STRICT_GATE_JOBS, so a skipped/absent
conclusion fails CI Summary closed.

NO runtime behavior change. In particular this does NOT implement inventory
section 7.2 (a PermissionError from the access seam is a CONTRACT defect and
should fail the consumer loudly rather than DLQ every event forever). That
finding is correct; the quarantine router is owned by OMN-16798 (#2949, #2951,
landed 2026-08-27), so this suite PINS the current behavior instead, making that
change a visible diff when someone makes it rather than a silent one.

Evidence: uv run pytest tests/integration/chains/ -q -> 9 passed
Evidence-Ticket: OMN-16814
github-actions Bot pushed a commit that referenced this pull request Aug 28, 2026
… dead-chain report misread (#2952)

* test(OMN-16813): gate the in-process intent-execution arm — the leg OMN-16755 read as dead

The OMN-16774 Event Chain Gate asserts exactly two things: a terminal event
lands, and nothing reaches the quarantine sink. Both are statements about
DispatchResultApplier's Phase 2 (output-event publish). Neither says anything
about Phase 1 -- intent execution -- or about the IntentExecutor behind it.

That hole has already cost a day. Re-probed live on the .201 dev lane
2026-08-27, the gateway link-health chain -- recorded as a DEAD CHAIN by a
High-priority ticket (OMN-16755), a status doc, and the platform testing
inventory (verdict row 7, "the canonical wired-but-never-fires shape") --
delivers its 17,717 upserts entirely in-process:

    handler -> DispatchResultApplier -> IntentExecutor -> intent effect

never over the bus. The chain was alive; every observer was blind, and blind in
the same direction, because the only leg any of them watches is the one this
chain does not use.

Why this cannot be a row in CHAIN_CASES
---------------------------------------
handler_wiring._normalize_handler_result routes a bare BaseModel return to
output_EVENTS; only ModelHandlerOutput.intents (or a ModelIntent in .result)
reaches output_intents. Every existing gate row returns a bare typed model, so
no number of appended rows reaches this leg. It needs an assertion aimed at the
effect side of the seam, which is what this module adds.

Every hop is production: EventBusInmemory, raw JSON wire bytes,
EventBusSubcontractWiring's real consumer + deserializer, _prepare_handler_wiring
arm selection, MessageDispatchEngine, the handler, DispatchResultApplier.apply,
IntentExecutor.execute_all, and a real ProtocolIntentEffect. The single patched
seam is _import_handler_class, exactly as the OMN-16774 gate patches it. Zero
infrastructure: no broker, no database, no container, no lane.

Negative control, and what it measured
--------------------------------------
test_a_dead_intent_leg_is_invisible_on_every_topic_the_gate_watches runs the
identical chain twice, differing only in whether the executor carries a handler
for the intent_type. Measured, not assumed:

  - terminal topic ............. EMPTY in both runs
  - quarantine sink ............ EMPTY in both runs (the ONLY sink OMN-16774 watches)
  - boundary DLQ ............... EMPTY in both runs

The refusal IS raised -- IntentExecutor names the unroutable intent_type -- but
EventBusInmemory exposes no _publish_raw_to_dlq and a RuntimeHostError is
classified retryable, not exhausted, on first delivery, so the exception is
logged as "Subscriber callback failed" and never reaches the publisher. The
initial draft of this suite asserted pytest.raises around bus.publish and was
WRONG for exactly that reason; the assertions were rewritten against what the
transport actually does rather than what it was assumed to do.

So on this leg, delivered work and bus-observable state are not the same
measurement, and the effect_calls assertion is the only one in this repo that
can go red when the intent arm breaks. That is what makes it non-vacuous.

RED-first proof (not a claim -- run and captured):
  register_effect default flipped to False, positive test re-run ->
  E  AssertionError: the intent effect was called 0 time(s), expected exactly 1.
  1 failed. Reverted; 4 passed.

Two further refusals are pinned so they cannot regress into silent commits: an
unregistered intent_type, and an applier built with no executor at all (which
must refuse in Phase 1 rather than fall through and publish a terminal event
advertising a write that never happened -- a dead leg that reports success).

AC3: IntentExecutor.execute answers a None payload with a WARNING and a bare
return, no raise. That branch is UNREACHABLE from validated data --
ModelIntent.payload is required and pydantic rejects None -- and that refusal is
now asserted rather than assumed, so the guarantee cannot be relaxed by a model
edit without going red. The branch stays as defence-in-depth.

Enforcement, not detection (rule 5): no workflow edit. The Event Chain Gate job
already runs `uv run pytest tests/integration/chains/` wholesale and is
registered in ci_summary_gate.py::STRICT_GATE_JOBS, so a skipped or absent
conclusion fails CI Summary closed. This file is collected the moment it lands.

NO runtime behavior change. The DLQ/quarantine router is owned by OMN-16798
(#2949, #2951, both landed 2026-08-27) and the inventory's section 7.2
PermissionError-swallow finding; deliberately not touched here.

Evidence: uv run pytest tests/integration/chains/ -q -> 8 passed
Evidence-Ticket: OMN-16813

* chore(OMN-16813): refresh receipt metadata after OCC merge

---------

Co-authored-by: branch-preview <preview@omninode.local>
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