0.8.0-dev2: Reflex — registry + runner + 3 first-party handlers#2
Merged
Conversation
Second sub-step of the brain refactor. Stands up the REFLEX LAYER —
fast deterministic responders that subscribe to the thalamus and
produce ReflexOutcome records. Nothing publishes to the bus yet (that
lands in 0.8.0-dev3), so the handlers idle. The plumbing they idle on
is fully tested against InMemoryEventBus, which (per 0.8.0-dev1's
contract suite) behaves identically to the production NatsEventBus —
so when the first publisher lands the path lights up end-to-end.
PUBLIC SURFACE — netcortex/reflex/
* protocol.py — ReflexHandler Protocol + frozen ReflexOutcome
dataclass + Severity / OutcomeKind literal types. Severity is a
four-bucket scale (info | warn | high | critical) so downstream
alerting can pattern-match without parsing free-form strings.
* registry.py — process-wide register_handler / get_handler /
all_handlers / clear_registry. Idempotent re-registration of the
same instance; duplicate-id collisions raise DuplicateHandlerError
(handler ids appear on every persisted outcome — silent shadowing
would be an operator footgun).
* runner.py — ReflexRunner wires the registry to one bus, spawns
one asyncio task per handler, isolates per-handler exceptions
(raising handler → "errored" outcome with truncated traceback in
diagnostic, dispatcher continues). Idempotent start/stop;
ready_event for tests that need cross-handler ordering.
FIRST-PARTY HANDLERS (all currently idle)
* link_down — sensory.snmp.trap.link_down.> → high severity. Caps
upstream key echo at 16 to bound outcome size.
* security_webhook — sensory.meraki.webhook.security.> → severity
derived from the Meraki payload via a coarse map
(informational/warning/high/critical → info/warn/high/critical),
defaults to warn for unknown values.
* bgp_drop — sensory.snmp.trap.bgp_backward_transition.> → high
severity. Target composed as "device|peer" when both are known,
falls back to whichever is present.
Each handler is intentionally minimal — captures the event, extracts
a target, returns a "logged" outcome. The richer behavior (semantic
memory lookup, maintenance-window check, dedup, NetBox journal mirror)
lands in later sub-steps once the first publisher exists to drive it.
TESTS — tests/reflex/
* test_registry.py: 7 cases — register/lookup, insertion ordering,
duplicate rejection, idempotent re-register, type rejection,
missing-key, clear. Save/restore fixture so cleared state does not
leak to sibling test files.
* test_runner.py: 8 cases against InMemoryEventBus — dispatch
matching events, pattern-filter non-matching, fan-out to multiple
handlers, exception isolation, None outcome not recorded,
idempotent start/stop, stop-without-start safety, registry
enumeration default-arg path.
* test_handlers.py: 14 cases pinning the operator-facing surface
(handler id + subscription pattern) and exercising each handler's
outcome-shape contract + target-extraction fallbacks.
End-to-end smoke against InMemoryEventBus: 3 publishes → 3 outcomes
routed to the right handlers with correct severity and target
extraction. NatsEventBus path will work identically (contract suite
proves both backends satisfy the same Protocol).
NOT YET WIRED
* Still no publishers. Pollers continue to call correlator +
writeback directly. First dual-write publisher lands in
0.8.0-dev3.
* Outcomes are logged only — Neo4j :ReflexEvent persistence + NetBox
journal mirror land in 0.8.0-dev3 once the writer Protocols have
a consumer to justify them.
Co-authored-by: Cursor <cursoragent@cursor.com>
test_handler_registered_with_expected_pattern was synchronous but the module's pytestmark = pytest.mark.asyncio still applied to it, producing 3 warnings per CI run. Future strict-mode pytest-asyncio may upgrade those warnings to errors. Trivially fixed by declaring the test async (no await required). Splitting it into its own file just to avoid the marker would be a worse trade-off. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second sub-step of the brain refactor. Stands up the reflex layer — fast deterministic responders that subscribe to the thalamus and produce
ReflexOutcomerecords.Nothing publishes to the bus yet (that lands in
0.8.0-dev3), so the handlers idle. The plumbing they idle on is fully tested againstInMemoryEventBus, which — per0.8.0-dev1's contract suite — behaves identically to the productionNatsEventBus. When the first publisher lands, the path lights up end-to-end with no changes here.Public surface (
netcortex/reflex/)protocol.py—ReflexHandlerProtocol + frozenReflexOutcomedataclass +Severity/OutcomeKindliteral types. Severity is a four-bucket scale (info | warn | high | critical) so downstream alerting can pattern-match without parsing free-form strings.registry.py— process-wideregister_handler/get_handler/all_handlers/clear_registry. Idempotent re-registration of the same instance; duplicate-id collisions raiseDuplicateHandlerError(handler ids appear on every persisted outcome — silent shadowing would be an operator footgun).runner.py—ReflexRunnerwires the registry to one bus, spawns one asyncio task per handler, isolates per-handler exceptions (raising handler →erroredoutcome with truncated traceback, dispatcher continues). Idempotentstart()/stop().First-party handlers (all currently idle)
link_downsensory.snmp.trap.link_down.>highsecurity_webhooksensory.meraki.webhook.security.>bgp_dropsensory.snmp.trap.bgp_backward_transition.>highdevice|peerwhen both knownEach handler is intentionally minimal — capture event, extract target, return
loggedoutcome. Semantic-memory lookup, maintenance-window check, dedup, NetBox journal mirror all land in later sub-steps once a publisher exists to drive them.Test plan
CI auto-runs:
tests/reflex/directory (~29 cases across 3 files)test_registry.py(7 cases): register/lookup, ordering, dup rejection, idempotent re-register, type rejection, missing-key, clear. Save/restore fixture prevents leaking cleared state to siblings.test_runner.py(8 cases againstInMemoryEventBus): dispatch, pattern filtering, fan-out, exception isolation,None-outcome skip, idempotent start/stop, stop-without-start safety, registry enumeration.test_handlers.py(14 cases): pin operator-facing surface (id + pattern) and exercise per-handler outcome shape + target extraction fallbacks.InMemoryEventBusAND realNatsEventBus(unchanged fromdev1)Pre-push smoke against
InMemoryEventBus: 3 publishes → 3 outcomes routed to the right handlers with correct severity and target. NatsEventBus path will behave identically (proven by the dev1 contract suite).Roadmap
feat/0.8.0-dev1-thalamus-natsNatsEventBus+ contract parametrizationfeat/0.8.0-dev2-reflex-skeletonnetcortex/reflex/skeleton + 3 idle handlersfeat/0.8.0-dev3-first-publisherfeat/0.8.0-dev4-module-renamesadapters/*→sensory/poll/*,graph/correlate.py→association/feat/0.8.0-dev5-cutover0.8.0Made with Cursor