omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. omnibase_infra/constants_topic_patterns.py +26 -0
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  4. omnibase_infra/enums/enum_handler_source_mode.py +16 -2
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_binding_resolution.py +128 -0
  7. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
  8. omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
  9. omnibase_infra/event_bus/event_bus_kafka.py +105 -47
  10. omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
  11. omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
  12. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
  13. omnibase_infra/event_bus/testing/__init__.py +26 -0
  14. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  15. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  16. omnibase_infra/handlers/handler_consul.py +2 -0
  17. omnibase_infra/handlers/mixins/__init__.py +5 -0
  18. omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
  19. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  20. omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
  21. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  22. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  23. omnibase_infra/mixins/mixin_node_introspection.py +189 -19
  24. omnibase_infra/models/__init__.py +8 -0
  25. omnibase_infra/models/bindings/__init__.py +59 -0
  26. omnibase_infra/models/bindings/constants.py +144 -0
  27. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  28. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  29. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  30. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  31. omnibase_infra/models/discovery/model_introspection_config.py +25 -17
  32. omnibase_infra/models/dispatch/__init__.py +8 -0
  33. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  34. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  35. omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
  36. omnibase_infra/models/model_node_identity.py +126 -0
  37. omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
  38. omnibase_infra/models/registration/__init__.py +9 -0
  39. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  40. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  41. omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
  42. omnibase_infra/models/runtime/__init__.py +9 -0
  43. omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
  44. omnibase_infra/nodes/__init__.py +9 -0
  45. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  46. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  47. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  48. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  49. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  50. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  51. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  52. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  53. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  54. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  55. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  56. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  57. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  58. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  59. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  60. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  61. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  62. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  63. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  64. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  65. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  66. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  67. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  68. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  69. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  70. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  71. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  72. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  73. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  74. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  75. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  76. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  77. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  78. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  79. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  80. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  81. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  82. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  83. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  84. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  85. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  86. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
  87. omnibase_infra/nodes/reducers/models/__init__.py +7 -2
  88. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
  89. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  90. omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
  91. omnibase_infra/protocols/__init__.py +3 -0
  92. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  93. omnibase_infra/runtime/__init__.py +60 -0
  94. omnibase_infra/runtime/binding_resolver.py +753 -0
  95. omnibase_infra/runtime/constants_security.py +70 -0
  96. omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
  97. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  98. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  99. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  100. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  101. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  102. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  103. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  104. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  105. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  106. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  107. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  108. omnibase_infra/runtime/handler_source_resolver.py +43 -2
  109. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  110. omnibase_infra/runtime/models/__init__.py +13 -0
  111. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  112. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  113. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
  114. omnibase_infra/runtime/models/model_security_config.py +109 -0
  115. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  116. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  117. omnibase_infra/runtime/service_kernel.py +76 -6
  118. omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
  119. omnibase_infra/runtime/service_runtime_host_process.py +770 -20
  120. omnibase_infra/runtime/transition_notification_publisher.py +3 -2
  121. omnibase_infra/runtime/util_wiring.py +206 -62
  122. omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
  123. omnibase_infra/services/session/config_consumer.py +25 -8
  124. omnibase_infra/services/session/config_store.py +2 -2
  125. omnibase_infra/services/session/consumer.py +1 -1
  126. omnibase_infra/topics/__init__.py +45 -0
  127. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  128. omnibase_infra/topics/util_topic_composition.py +95 -0
  129. omnibase_infra/types/typed_dict/__init__.py +9 -1
  130. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  131. omnibase_infra/utils/__init__.py +9 -0
  132. omnibase_infra/utils/util_consumer_group.py +232 -0
  133. omnibase_infra/validation/infra_validators.py +18 -1
  134. omnibase_infra/validation/validation_exemptions.yaml +192 -0
  135. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
  136. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
  137. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
  138. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
  139. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
@@ -8,10 +8,16 @@ for HandlerConsul, extracted to reduce class complexity.
8
8
  Operations:
9
9
  - consul.register: Register service with Consul agent
10
10
  - consul.deregister: Deregister service from Consul agent
11
+
12
+ Event Bus Integration:
13
+ When the payload contains an 'event_bus_config' field, this mixin will:
14
+ 1. Store the event bus configuration in Consul KV
15
+ 2. Update the topic -> node_id reverse index for routing
11
16
  """
12
17
 
13
18
  from __future__ import annotations
14
19
 
20
+ import logging
15
21
  from collections.abc import Callable
16
22
  from typing import TYPE_CHECKING, Protocol, TypeVar
17
23
  from uuid import UUID
@@ -19,10 +25,12 @@ from uuid import UUID
19
25
  T = TypeVar("T")
20
26
 
21
27
  from omnibase_core.models.dispatch import ModelHandlerOutput
22
- from omnibase_infra.enums import EnumInfraTransportType
28
+ from omnibase_infra.constants_topic_patterns import TOPIC_NAME_PATTERN
29
+ from omnibase_infra.enums import EnumInfraTransportType, EnumMessageCategory
23
30
  from omnibase_infra.errors import (
24
31
  InfraConsulError,
25
32
  ModelInfraErrorContext,
33
+ ProtocolConfigurationError,
26
34
  RuntimeHostError,
27
35
  )
28
36
  from omnibase_infra.handlers.models.consul import (
@@ -33,10 +41,16 @@ from omnibase_infra.handlers.models.consul import (
33
41
  from omnibase_infra.handlers.models.model_consul_handler_response import (
34
42
  ModelConsulHandlerResponse,
35
43
  )
44
+ from omnibase_infra.models.registration import (
45
+ ModelEventBusTopicEntry,
46
+ ModelNodeEventBusConfig,
47
+ )
36
48
 
37
49
  if TYPE_CHECKING:
38
50
  import consul as consul_lib
39
51
 
52
+ logger = logging.getLogger(__name__)
53
+
40
54
 
41
55
  class ProtocolConsulServiceDependencies(Protocol):
42
56
  """Protocol defining required dependencies for service operations.
@@ -65,6 +79,24 @@ class ProtocolConsulServiceDependencies(Protocol):
65
79
  """Build standardized response."""
66
80
  ...
67
81
 
82
+ async def _store_node_event_bus(
83
+ self,
84
+ node_id: str,
85
+ event_bus: ModelNodeEventBusConfig,
86
+ correlation_id: UUID,
87
+ ) -> None:
88
+ """Store event_bus config in Consul KV - provided by MixinConsulTopicIndex."""
89
+ ...
90
+
91
+ async def _update_topic_index(
92
+ self,
93
+ node_id: str,
94
+ event_bus: ModelNodeEventBusConfig,
95
+ correlation_id: UUID,
96
+ ) -> None:
97
+ """Update topic index - provided by MixinConsulTopicIndex."""
98
+ ...
99
+
68
100
 
69
101
  class MixinConsulService:
70
102
  """Mixin providing Consul service registration operations.
@@ -102,6 +134,180 @@ class MixinConsulService:
102
134
  """Build standardized response - provided by host class."""
103
135
  raise NotImplementedError("Must be provided by implementing class") # type: ignore[return-value]
104
136
 
137
+ async def _store_node_event_bus(
138
+ self,
139
+ node_id: str,
140
+ event_bus: ModelNodeEventBusConfig,
141
+ correlation_id: UUID,
142
+ ) -> None:
143
+ """Store event_bus config - provided by MixinConsulTopicIndex."""
144
+ raise NotImplementedError("Must be provided by implementing class")
145
+
146
+ async def _update_topic_index(
147
+ self,
148
+ node_id: str,
149
+ event_bus: ModelNodeEventBusConfig,
150
+ correlation_id: UUID,
151
+ ) -> None:
152
+ """Update topic index - provided by MixinConsulTopicIndex."""
153
+ raise NotImplementedError("Must be provided by implementing class")
154
+
155
+ def _validate_topic_entry(
156
+ self,
157
+ entry: dict[str, object],
158
+ location: str,
159
+ correlation_id: UUID,
160
+ ) -> tuple[str, str]:
161
+ """Validate a single topic entry and return sanitized values.
162
+
163
+ Validates:
164
+ 1. Topic is a non-empty string after stripping whitespace
165
+ 2. Topic format matches TOPIC_NAME_PATTERN (alphanumeric, dots, underscores, hyphens)
166
+ 3. message_category is a valid EnumMessageCategory value
167
+
168
+ Args:
169
+ entry: The full topic entry dict containing 'topic' and optional 'message_category'.
170
+ location: Location string for error messages (e.g., "subscribe_topics[0]").
171
+ correlation_id: Correlation ID for tracing.
172
+
173
+ Returns:
174
+ Tuple of (stripped_topic, validated_message_category).
175
+
176
+ Raises:
177
+ ProtocolConfigurationError: If validation fails.
178
+ """
179
+ raw_topic = entry.get("topic")
180
+
181
+ # Validate topic is a non-empty string BEFORE any coercion
182
+ if not isinstance(raw_topic, str) or not raw_topic.strip():
183
+ ctx = ModelInfraErrorContext.with_correlation(
184
+ correlation_id=correlation_id,
185
+ transport_type=EnumInfraTransportType.CONSUL,
186
+ operation="parse_event_bus_config",
187
+ )
188
+ raise ProtocolConfigurationError(
189
+ f"Invalid or missing 'topic' in {location}: "
190
+ f"expected non-empty string, got {type(raw_topic).__name__}",
191
+ context=ctx,
192
+ parameter=f"{location}.topic",
193
+ value=str(raw_topic) if raw_topic is not None else "None",
194
+ )
195
+
196
+ stripped_topic = raw_topic.strip()
197
+
198
+ # Validate topic format (fail fast before storage/indexing)
199
+ if not TOPIC_NAME_PATTERN.match(stripped_topic):
200
+ ctx = ModelInfraErrorContext.with_correlation(
201
+ correlation_id=correlation_id,
202
+ transport_type=EnumInfraTransportType.CONSUL,
203
+ operation="parse_event_bus_config",
204
+ )
205
+ raise ProtocolConfigurationError(
206
+ f"Topic '{stripped_topic}' in {location} "
207
+ "contains invalid characters. Only alphanumeric characters, periods (.), "
208
+ "underscores (_), and hyphens (-) are allowed.",
209
+ context=ctx,
210
+ parameter=f"{location}.topic",
211
+ value=stripped_topic,
212
+ )
213
+
214
+ # Validate message_category if provided
215
+ raw_category = entry.get("message_category", "EVENT")
216
+ if isinstance(raw_category, str):
217
+ category_upper = raw_category.upper()
218
+ else:
219
+ category_upper = str(raw_category).upper()
220
+
221
+ # Valid categories: EVENT, COMMAND, INTENT (case-insensitive)
222
+ valid_categories = {
223
+ cat.value.upper(): cat.value.upper() for cat in EnumMessageCategory
224
+ }
225
+ if category_upper not in valid_categories:
226
+ ctx = ModelInfraErrorContext.with_correlation(
227
+ correlation_id=correlation_id,
228
+ transport_type=EnumInfraTransportType.CONSUL,
229
+ operation="parse_event_bus_config",
230
+ )
231
+ raise ProtocolConfigurationError(
232
+ f"Invalid 'message_category' in {location}: "
233
+ f"'{raw_category}'. Valid values are: {', '.join(sorted(valid_categories.keys()))}",
234
+ context=ctx,
235
+ parameter=f"{location}.message_category",
236
+ value=str(raw_category),
237
+ )
238
+
239
+ return stripped_topic, category_upper
240
+
241
+ def _parse_event_bus_config(
242
+ self,
243
+ event_bus_data: dict[str, object],
244
+ correlation_id: UUID,
245
+ ) -> ModelNodeEventBusConfig:
246
+ """Parse event_bus_config from payload dict to typed model.
247
+
248
+ Args:
249
+ event_bus_data: Raw event_bus_config dict from payload.
250
+ correlation_id: Correlation ID for tracing.
251
+
252
+ Returns:
253
+ Parsed ModelNodeEventBusConfig instance.
254
+
255
+ Raises:
256
+ ProtocolConfigurationError: If any topic entry has an invalid topic,
257
+ invalid format, or invalid message_category.
258
+ """
259
+ subscribe_topics: list[ModelEventBusTopicEntry] = []
260
+ publish_topics: list[ModelEventBusTopicEntry] = []
261
+
262
+ raw_subscribe = event_bus_data.get("subscribe_topics")
263
+ if isinstance(raw_subscribe, list):
264
+ for idx, entry in enumerate(raw_subscribe):
265
+ if isinstance(entry, dict):
266
+ stripped_topic, message_category = self._validate_topic_entry(
267
+ entry=entry,
268
+ location=f"subscribe_topics[{idx}]",
269
+ correlation_id=correlation_id,
270
+ )
271
+ subscribe_topics.append(
272
+ ModelEventBusTopicEntry(
273
+ topic=stripped_topic,
274
+ event_type=entry.get("event_type")
275
+ if isinstance(entry.get("event_type"), str)
276
+ else None,
277
+ message_category=message_category,
278
+ description=entry.get("description")
279
+ if isinstance(entry.get("description"), str)
280
+ else None,
281
+ )
282
+ )
283
+
284
+ raw_publish = event_bus_data.get("publish_topics")
285
+ if isinstance(raw_publish, list):
286
+ for idx, entry in enumerate(raw_publish):
287
+ if isinstance(entry, dict):
288
+ stripped_topic, message_category = self._validate_topic_entry(
289
+ entry=entry,
290
+ location=f"publish_topics[{idx}]",
291
+ correlation_id=correlation_id,
292
+ )
293
+ publish_topics.append(
294
+ ModelEventBusTopicEntry(
295
+ topic=stripped_topic,
296
+ event_type=entry.get("event_type")
297
+ if isinstance(entry.get("event_type"), str)
298
+ else None,
299
+ message_category=message_category,
300
+ description=entry.get("description")
301
+ if isinstance(entry.get("description"), str)
302
+ else None,
303
+ )
304
+ )
305
+
306
+ return ModelNodeEventBusConfig(
307
+ subscribe_topics=subscribe_topics,
308
+ publish_topics=publish_topics,
309
+ )
310
+
105
311
  async def _register_service(
106
312
  self,
107
313
  payload: dict[str, object],
@@ -118,23 +324,35 @@ class MixinConsulService:
118
324
  - port: Optional service port
119
325
  - tags: Optional list of tags
120
326
  - check: Optional health check configuration dict
327
+ - node_id: Optional node ID for event bus registration
328
+ - event_bus_config: Optional event bus configuration dict containing:
329
+ - subscribe_topics: List of topic entries to subscribe to
330
+ - publish_topics: List of topic entries to publish to
121
331
  correlation_id: Correlation ID for tracing
122
332
  input_envelope_id: Input envelope ID for causality tracking
123
333
 
124
334
  Returns:
125
335
  ModelHandlerOutput wrapping the registration result with correlation tracking
336
+
337
+ Event Bus Integration:
338
+ When event_bus_config is provided along with node_id, this method will:
339
+ 1. Store the event bus configuration in Consul KV at onex/nodes/{node_id}/event_bus/
340
+ 2. Update the topic -> node_id reverse index at onex/topics/{topic}/subscribers
126
341
  """
127
342
  name = payload.get("name")
128
343
  if not isinstance(name, str) or not name:
129
- ctx = ModelInfraErrorContext(
344
+ ctx = ModelInfraErrorContext.with_correlation(
345
+ correlation_id=correlation_id,
130
346
  transport_type=EnumInfraTransportType.CONSUL,
131
347
  operation="consul.register",
132
348
  target_name="consul_handler",
133
- correlation_id=correlation_id,
134
349
  )
135
- raise RuntimeHostError(
136
- "Missing or invalid 'name' in payload",
350
+ raise ProtocolConfigurationError(
351
+ "Missing or invalid 'name' in payload: "
352
+ f"expected non-empty string, got {type(name).__name__}",
137
353
  context=ctx,
354
+ parameter="name",
355
+ value=str(name) if name is not None else "None",
138
356
  )
139
357
 
140
358
  service_id = payload.get("service_id")
@@ -198,6 +416,49 @@ class MixinConsulService:
198
416
  correlation_id,
199
417
  )
200
418
 
419
+ # Handle event bus configuration if provided
420
+ event_bus_data = payload.get("event_bus_config")
421
+ node_id = payload.get("node_id")
422
+
423
+ # Fail fast: if event_bus_config is present, node_id is REQUIRED
424
+ if isinstance(event_bus_data, dict):
425
+ if not isinstance(node_id, str) or not node_id.strip():
426
+ ctx = ModelInfraErrorContext.with_correlation(
427
+ correlation_id=correlation_id,
428
+ transport_type=EnumInfraTransportType.CONSUL,
429
+ operation="consul.register",
430
+ )
431
+ raise ProtocolConfigurationError(
432
+ "event_bus_config requires a valid 'node_id': "
433
+ f"expected non-empty string, got {type(node_id).__name__}",
434
+ context=ctx,
435
+ parameter="node_id",
436
+ value=str(node_id) if node_id is not None else "None",
437
+ )
438
+
439
+ logger.info(
440
+ "Processing event_bus_config for node %s",
441
+ node_id,
442
+ extra={"correlation_id": str(correlation_id), "node_id": node_id},
443
+ )
444
+
445
+ # Parse the event bus config
446
+ event_bus = self._parse_event_bus_config(event_bus_data, correlation_id)
447
+
448
+ # Update topic index FIRST (uses old topics from previous registration)
449
+ # This computes delta and updates reverse index
450
+ await self._update_topic_index(node_id, event_bus, correlation_id)
451
+
452
+ # Store the new event bus config AFTER index update
453
+ # Order matters: _update_topic_index reads old topics before we overwrite
454
+ await self._store_node_event_bus(node_id, event_bus, correlation_id)
455
+
456
+ logger.info(
457
+ "Completed event_bus registration for node %s",
458
+ node_id,
459
+ extra={"correlation_id": str(correlation_id), "node_id": node_id},
460
+ )
461
+
201
462
  typed_payload = ModelConsulRegisterPayload(
202
463
  registered=True,
203
464
  name=name,
@@ -223,16 +484,19 @@ class MixinConsulService:
223
484
  ModelHandlerOutput wrapping the deregistration result with correlation tracking
224
485
  """
225
486
  service_id = payload.get("service_id")
226
- if not isinstance(service_id, str) or not service_id:
227
- ctx = ModelInfraErrorContext(
487
+ if not isinstance(service_id, str) or not service_id.strip():
488
+ ctx = ModelInfraErrorContext.with_correlation(
489
+ correlation_id=correlation_id,
228
490
  transport_type=EnumInfraTransportType.CONSUL,
229
491
  operation="consul.deregister",
230
492
  target_name="consul_handler",
231
- correlation_id=correlation_id,
232
493
  )
233
- raise RuntimeHostError(
234
- "Missing or invalid 'service_id' in payload",
494
+ raise ProtocolConfigurationError(
495
+ "Missing or invalid 'service_id' in payload: "
496
+ f"expected non-empty string, got {type(service_id).__name__}",
235
497
  context=ctx,
498
+ parameter="service_id",
499
+ value=str(service_id) if service_id is not None else "None",
236
500
  )
237
501
 
238
502
  if self._client is None: