Skip to content

fix(OMN-15660): run-scope the handler consumer group and refuse a sibling invocation's handler input - #1671

Merged
jonahgabriel merged 1 commit into
devfrom
jonah/omn-15660-handler-leg-correlation
Sep 9, 2026
Merged

jonahgabriel merged 1 commit into
devfrom
jonah/omn-15660-handler-leg-correlation

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

OMN-15660 — the handler leg: run-scope the command-topic group and refuse a sibling's handler input

omnibase_core#1645 closed this ticket's terminal leg. AC3 names two call
sites and says in its own sentence that a fix that scopes only one path is
incomplete
. This closes the handler leg, and with it AC1, AC2 and AC3.

What was still open at origin/dev 09fdb3f7

Call site State before this PR
terminal consumer group (runtime_local.py:1562) run-scoped — #1645
terminal message path (_terminal_correlation_matches) filtered — #1645, corrected by #1655
handler consumer group (runtime_local.py:1526) derive_runtime_local_group_id(entry.handler_name) — identical for every invocation of the same handler
handler message path (LocalRuntimeBusAdapter.on_message) correlation_id read for logging only; no conditional between deserialize and invoke

Two concurrent invocations therefore joined one group on the command topic.
A broker hands a record to exactly one member of a group, so a run's own command
was executed in the sibling's process, and nothing on the receiving side refused
it.

The change

  • runtime_local.py suffixes the handler subscribe group with the invocation's
    run_id, using the shape already eight lines below it at :1555-1562.
  • LocalRuntimeBusAdapter takes expected_correlation_id and refuses a message
    declaring a correlation that is not this invocation's, before the handler
    invoke.

Both are gated on the correlation predicate being armed, for the reason the
terminal group already documents: a brand-new group reads the retained log from
the beginning, so scoping without a filter trades "the uncommitted tail" for
"the entire retention window" — strictly worse. Neither half is sufficient
alone; that is AC3's point.

What this deliberately does NOT do

  • A message declaring NO correlation is still invoked on. The canonical
    def-B message is the handler's bare domain model, which has no field in which
    to carry a correlation. This is the lesson #1655 paid for on the terminal
    leg; refusing that shape would stall every def-B chain after its first hop
    rather than isolating anything.
  • A refusal is not routed through on_error. A sibling's record is not this
    run's failure; failing on one would make every concurrent invocation fail the
    moment a sibling published.
  • An unarmed adapter refuses nothing — the offline / in-memory path is
    byte-for-byte unchanged.

Each of the three is pinned by its own counter-assertion, so "refuse everything"
does not pass this suite.

Evidence

Proof Result
RED control — the two source files stashed, the tests kept 9 failed, 14 passed. test_a_concurrent_run_never_executes_a_siblings_command failed with "one process executed both commands"; test_host_mode_handler_group_is_run_scoped and test_two_concurrent_host_runs_do_not_share_a_handler_group failed on the shared group id local.omnibase_core.handlerisolationecho.consume.v1
GREEN uv run pytest tests/unit/runtime/test_runtime_local_adapter_correlation_refusal.py tests/unit/runtime/test_runtime_local_terminal_isolation.py -q → 23 passed
No regression on adjacent paths test_runtime_local.py, test_runtime_local_client_mode.py, test_runtime_local_adapter_fanout.py, test_runtime_local_adapter_event_type.py, test_runtime_dispatch.py → 99 passed
Lint / types ruff format, ruff check --fix, mypy ... --strict → clean, "no issues found in 2 source files"
Pre-push governed impacted-test selector, unmodified, no override env. It escalated to the fail-closed full suite and the lab leg accepted it: h101.2 ran 45073 tests green on e60465b328d1124ff247224d29516e7183aa38b9 (5684s)
Lab not applicable — RuntimeLocal is an in-process library path, exercised by the suite above, not by a deployed lane. No lane was mutated by this work.

The RED control for the new adapter test file is a TypeError on the
constructor kwarg rather than a behavioural failure, stated rather than glossed:
the refusal did not exist to fail. The behavioural RED lives in the three
runtime-level tests above, which run the real RuntimeLocal against a durable
broker double.

pre-commit run --all-files reports three failures on files this PR does not
touch (util_topic_suffix.py, harness/harness_topics.py,
node_no_env_fallbacks_check_compute/*, model_llm_route_resolved_event.py).
Positive control: all three reproduce byte-identically on a clean origin/dev
tree with this branch's changes stashed. The staged-file run at commit time is
clean.

Deploy surface

Library code under src/omnibase_core/runtime/. No deployment, no contract
schema change, no wire-format change. Version bumped to 0.47.7 because
packaged source changed on an already-published 0.47.6.

Ticket: OMN-15660

Evidence-Source: OCC#8804
Evidence-Ticket: OMN-15660

…ling invocation's handler input

AC3's handler half and AC2's handler half — the two the terminal-leg fix
(#1645) did not touch.

runtime_local.py subscribed to the command topic with
derive_runtime_local_group_id(entry.handler_name), a group id identical for
every invocation of the same handler, so two concurrent invocations shared one
group and the broker handed each command to exactly one of them: a run's own
work executed in the sibling's process. LocalRuntimeBusAdapter.on_message read
correlation_id for logging only, with no conditional between deserialize and
invoke, so nothing refused a foreign record either.

The subscribe group is now suffixed with the invocation's run_id, and the
adapter refuses a message declaring a correlation that is not this
invocation's. Both are gated on the correlation predicate being armed, for the
reason the terminal group already documents: a brand-new group reads the
retained log from the beginning, so scoping without a filter trades the
uncommitted tail for the entire retention window.

A message declaring NO correlation is still invoked on — the lesson #1655 paid
for on the terminal leg, since the canonical def-B message is a bare domain
model with no field to carry one. A refusal is not routed through on_error: a
sibling's record is not this run's failure, and failing on one would make every
concurrent invocation fail the moment a sibling published.

RED control (source stashed, tests kept): 9 failed / 14 passed. GREEN: 23
passed. Version bumped to 0.47.7 — packaged source changed on an
already-published 0.47.6.
Comment on lines +34 to +36
from omnibase_core.protocols.runtime.protocol_local_runtime_bus import (
ProtocolLocalRuntimeBus,
)
Comment on lines +37 to +39
from omnibase_core.protocols.runtime.protocol_local_runtime_callable_target import (
ProtocolLocalRuntimeCallableTarget,
)
@jonahgabriel
jonahgabriel enabled auto-merge (squash) September 9, 2026 15:15
@jonahgabriel
jonahgabriel merged commit 2be947e into dev Sep 9, 2026
318 of 352 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-15660-handler-leg-correlation branch September 9, 2026 17:18
@onexbot-occ-writer

Copy link
Copy Markdown
Contributor

No OCC evidence companion was minted for this PR.

this PR is already bound to OCC#8804; its evidence companion exists and nothing needs authoring

To clear this: Nothing to do — the companion already exists.

Reported by node_occ_companion_effect (decline code already_bound, OMN-16665). This decision was made against the PR's LIVE state at compute time, not at publish time — a green publisher job only means the command reached the broker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants