omnibase_infra 0.3.1__py3-none-any.whl → 0.3.2__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 (72) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  6. omnibase_infra/mixins/__init__.py +14 -0
  7. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  8. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  9. omnibase_infra/models/__init__.py +3 -0
  10. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  11. omnibase_infra/models/projection/__init__.py +11 -0
  12. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  13. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  14. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  15. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  16. omnibase_infra/nodes/effects/__init__.py +1 -1
  17. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  18. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  19. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  20. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  21. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  22. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  23. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  24. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  25. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  26. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  27. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  28. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  29. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  30. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  31. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  32. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  33. omnibase_infra/nodes/node_contract_persistence_effect/node.py +114 -0
  34. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  35. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +220 -0
  36. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  37. omnibase_infra/projectors/__init__.py +6 -0
  38. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  39. omnibase_infra/runtime/__init__.py +5 -0
  40. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  41. omnibase_infra/runtime/db/__init__.py +4 -0
  42. omnibase_infra/runtime/db/models/__init__.py +15 -10
  43. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  44. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  45. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  46. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  47. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  48. omnibase_infra/runtime/intent_execution_router.py +430 -0
  49. omnibase_infra/runtime/models/__init__.py +6 -0
  50. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  51. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  52. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  53. omnibase_infra/runtime/protocols/__init__.py +16 -0
  54. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  55. omnibase_infra/runtime/request_response_wiring.py +785 -0
  56. omnibase_infra/runtime/service_kernel.py +295 -8
  57. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  58. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  59. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  60. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  61. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  62. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  63. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  64. omnibase_infra/services/registry_api/routes.py +205 -6
  65. omnibase_infra/services/registry_api/service.py +528 -1
  66. omnibase_infra/validation/infra_validators.py +3 -1
  67. omnibase_infra/validation/validation_exemptions.yaml +54 -0
  68. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/METADATA +3 -3
  69. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/RECORD +72 -34
  70. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/WHEEL +0 -0
  71. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/entry_points.txt +0 -0
  72. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/licenses/LICENSE +0 -0
@@ -76,7 +76,7 @@ See Also
76
76
  - Runtime kernel: omnibase_infra.runtime.service_kernel
77
77
  """
78
78
 
79
- __version__ = "0.3.1"
79
+ __version__ = "0.3.2"
80
80
 
81
81
  from . import (
82
82
  enums,
@@ -36,6 +36,7 @@ Exports:
36
36
  EnumNodeOutputType: Node output types for execution shape validation
37
37
  EnumNonRetryableErrorCategory: Non-retryable error categories for DLQ
38
38
  EnumPolicyType: Policy types for RegistryPolicy plugins
39
+ EnumPostgresErrorCode: PostgreSQL error codes for contract persistence operations
39
40
  EnumRegistrationState: Registration FSM states for two-way registration
40
41
  EnumRegistrationStatus: Registration workflow status (IDLE, PENDING, PARTIAL, COMPLETE, FAILED)
41
42
  EnumRegistryResponseStatus: Registry operation response status (SUCCESS, PARTIAL, FAILED)
@@ -82,6 +83,7 @@ from omnibase_infra.enums.enum_non_retryable_error_category import (
82
83
  EnumNonRetryableErrorCategory,
83
84
  )
84
85
  from omnibase_infra.enums.enum_policy_type import EnumPolicyType
86
+ from omnibase_infra.enums.enum_postgres_error_code import EnumPostgresErrorCode
85
87
  from omnibase_infra.enums.enum_registration_state import EnumRegistrationState
86
88
  from omnibase_infra.enums.enum_registration_status import EnumRegistrationStatus
87
89
  from omnibase_infra.enums.enum_registry_response_status import (
@@ -123,6 +125,7 @@ __all__: list[str] = [
123
125
  "EnumNodeOutputType",
124
126
  "EnumNonRetryableErrorCategory",
125
127
  "EnumPolicyType",
128
+ "EnumPostgresErrorCode",
126
129
  "EnumRegistrationState",
127
130
  "EnumRegistrationStatus",
128
131
  "EnumRegistryResponseStatus",
@@ -11,6 +11,7 @@ Consumer Group Purpose Categories:
11
11
  - REPLAY: Reprocess historical data from earliest offset
12
12
  - AUDIT: Compliance and read-only consumption
13
13
  - BACKFILL: One-shot bounded consumers for populating derived state
14
+ - CONTRACT_REGISTRY: Contract lifecycle events (registration, deregistration, heartbeat)
14
15
 
15
16
  The purpose determines consumer group naming conventions and default
16
17
  offset reset policies in the Kafka adapter layer.
@@ -63,6 +64,11 @@ class EnumConsumerGroupPurpose(str, Enum):
63
64
  - Naming: {base_group_id}-backfill
64
65
  - Pattern: Bounded consumption until caught up
65
66
 
67
+ CONTRACT_REGISTRY: Contract lifecycle events processing.
68
+ - Default offset reset: latest
69
+ - Naming: {base_group_id}-contract-registry
70
+ - Pattern: Continuous consumption of registration, deregistration, heartbeat events
71
+
66
72
  Example:
67
73
  >>> purpose = EnumConsumerGroupPurpose.REPLAY
68
74
  >>> f"order-processor-{purpose.value}"
@@ -84,6 +90,9 @@ class EnumConsumerGroupPurpose(str, Enum):
84
90
  BACKFILL = "backfill"
85
91
  """One-shot bounded consumers for populating derived state."""
86
92
 
93
+ CONTRACT_REGISTRY = "contract-registry"
94
+ """Contract lifecycle events (registration, deregistration, heartbeat)."""
95
+
87
96
  def __str__(self) -> str:
88
97
  """Return the string value for serialization."""
89
98
  return self.value
@@ -0,0 +1,188 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """PostgreSQL Error Code Enumeration.
4
+
5
+ Defines structured error codes for PostgreSQL persistence operations. These codes
6
+ enable precise error classification, debugging, and programmatic error handling
7
+ for contract registry persistence via NodeContractPersistenceEffect.
8
+
9
+ Error Code Categories:
10
+ - Connection errors: Connection, timeout, authentication failures
11
+ - Operation errors: Specific operation failures (upsert, topic, etc.)
12
+ - Unknown errors: Catch-all for unclassified failures
13
+
14
+ Usage:
15
+ >>> from omnibase_infra.enums import EnumPostgresErrorCode
16
+ >>> error_code = EnumPostgresErrorCode.CONNECTION_ERROR
17
+ >>> print(f"Error: {error_code.value}")
18
+ Error: POSTGRES_CONNECTION_ERROR
19
+
20
+ >>> # Check if error is retriable
21
+ >>> if error_code.is_retriable:
22
+ ... print("Will retry operation")
23
+
24
+ >>> # Categorize error type
25
+ >>> if error_code.is_connection_error:
26
+ ... print("Connection-level failure")
27
+
28
+ See Also:
29
+ - NodeContractPersistenceEffect: Effect node using these error codes
30
+ - ContractRegistryReducer: Source of intents that may trigger these errors
31
+ - contract.yaml: Error code definitions in error_handling.error_codes
32
+ """
33
+
34
+ from enum import Enum
35
+
36
+
37
+ class EnumPostgresErrorCode(str, Enum):
38
+ """Error codes for PostgreSQL persistence operations.
39
+
40
+ These codes provide structured classification for failures during
41
+ contract registry persistence operations. Each code maps to a specific
42
+ failure scenario as defined in the contract.yaml error_codes section.
43
+
44
+ Connection Errors (retriable):
45
+ CONNECTION_ERROR: Connection to PostgreSQL server failed.
46
+ The database server is unreachable or connection was refused.
47
+ Verify PostgreSQL server is running and network is accessible.
48
+
49
+ TIMEOUT_ERROR: PostgreSQL operation exceeded timeout.
50
+ The operation took longer than the configured timeout threshold.
51
+ Check database load and query performance.
52
+
53
+ Authentication Errors (non-retriable):
54
+ AUTH_ERROR: Authentication with PostgreSQL server failed.
55
+ Invalid credentials or insufficient privileges.
56
+ Verify POSTGRES_USER and POSTGRES_PASSWORD in .env.
57
+
58
+ Operation Errors (non-retriable):
59
+ UPSERT_ERROR: PostgreSQL upsert operation failed.
60
+ Insert/update of contract record failed due to constraint
61
+ violation, invalid data, or schema mismatch.
62
+
63
+ TOPIC_UPDATE_ERROR: PostgreSQL topic update failed.
64
+ Failed to update topic routing table. Check JSONB array
65
+ operations and topic table schema.
66
+
67
+ MARK_STALE_ERROR: PostgreSQL mark stale operation failed.
68
+ Batch staleness marking failed. Check is_stale column
69
+ and last_seen_at timestamp handling.
70
+
71
+ HEARTBEAT_ERROR: PostgreSQL heartbeat update failed.
72
+ Heartbeat timestamp update failed. Verify contract_id
73
+ exists and last_seen_at column is writable.
74
+
75
+ DEACTIVATE_ERROR: PostgreSQL deactivation failed.
76
+ Soft delete (is_active=false) failed. Check contract_id
77
+ validity and is_active column constraints.
78
+
79
+ CLEANUP_ERROR: PostgreSQL topic cleanup failed.
80
+ Failed to remove contract from topic arrays. Check JSONB
81
+ array manipulation and referential integrity.
82
+
83
+ Unknown Errors (non-retriable):
84
+ UNKNOWN_ERROR: Unknown error during PostgreSQL operation.
85
+ Catch-all for unclassified PostgreSQL failures.
86
+ Check logs for underlying exception details.
87
+ """
88
+
89
+ # Connection errors
90
+ CONNECTION_ERROR = "POSTGRES_CONNECTION_ERROR"
91
+ TIMEOUT_ERROR = "POSTGRES_TIMEOUT_ERROR"
92
+ AUTH_ERROR = "POSTGRES_AUTH_ERROR"
93
+
94
+ # Operation errors
95
+ UPSERT_ERROR = "POSTGRES_UPSERT_ERROR"
96
+ TOPIC_UPDATE_ERROR = "POSTGRES_TOPIC_UPDATE_ERROR"
97
+ MARK_STALE_ERROR = "POSTGRES_MARK_STALE_ERROR"
98
+ HEARTBEAT_ERROR = "POSTGRES_HEARTBEAT_ERROR"
99
+ DEACTIVATE_ERROR = "POSTGRES_DEACTIVATE_ERROR"
100
+ CLEANUP_ERROR = "POSTGRES_CLEANUP_ERROR"
101
+
102
+ # Unknown errors
103
+ UNKNOWN_ERROR = "POSTGRES_UNKNOWN_ERROR"
104
+
105
+ @property
106
+ def is_retriable(self) -> bool:
107
+ """Check if this error is retriable.
108
+
109
+ Retriable errors indicate transient failures that may succeed
110
+ on retry, such as connection issues or timeouts. Non-retriable
111
+ errors indicate permanent failures requiring intervention.
112
+
113
+ Returns:
114
+ True if the error is retriable, False otherwise.
115
+ """
116
+ return self in {
117
+ EnumPostgresErrorCode.CONNECTION_ERROR,
118
+ EnumPostgresErrorCode.TIMEOUT_ERROR,
119
+ }
120
+
121
+ @property
122
+ def is_connection_error(self) -> bool:
123
+ """Check if this is a connection-level error.
124
+
125
+ Connection errors indicate infrastructure-level failures
126
+ rather than operation-specific issues.
127
+
128
+ Returns:
129
+ True if this is a connection-level error.
130
+ """
131
+ return self in {
132
+ EnumPostgresErrorCode.CONNECTION_ERROR,
133
+ EnumPostgresErrorCode.TIMEOUT_ERROR,
134
+ EnumPostgresErrorCode.AUTH_ERROR,
135
+ }
136
+
137
+ @property
138
+ def is_operation_error(self) -> bool:
139
+ """Check if this is an operation-specific error.
140
+
141
+ Operation errors indicate failures in specific database
142
+ operations rather than infrastructure issues.
143
+
144
+ Returns:
145
+ True if this is an operation-specific error.
146
+ """
147
+ return self in {
148
+ EnumPostgresErrorCode.UPSERT_ERROR,
149
+ EnumPostgresErrorCode.TOPIC_UPDATE_ERROR,
150
+ EnumPostgresErrorCode.MARK_STALE_ERROR,
151
+ EnumPostgresErrorCode.HEARTBEAT_ERROR,
152
+ EnumPostgresErrorCode.DEACTIVATE_ERROR,
153
+ EnumPostgresErrorCode.CLEANUP_ERROR,
154
+ }
155
+
156
+ @property
157
+ def description(self) -> str:
158
+ """Get human-readable description of the error code.
159
+
160
+ Returns:
161
+ Description string for the error code.
162
+ """
163
+ descriptions = {
164
+ EnumPostgresErrorCode.CONNECTION_ERROR: (
165
+ "Connection to PostgreSQL server failed"
166
+ ),
167
+ EnumPostgresErrorCode.TIMEOUT_ERROR: (
168
+ "PostgreSQL operation exceeded timeout"
169
+ ),
170
+ EnumPostgresErrorCode.AUTH_ERROR: (
171
+ "Authentication with PostgreSQL server failed"
172
+ ),
173
+ EnumPostgresErrorCode.UPSERT_ERROR: "PostgreSQL upsert operation failed",
174
+ EnumPostgresErrorCode.TOPIC_UPDATE_ERROR: "PostgreSQL topic update failed",
175
+ EnumPostgresErrorCode.MARK_STALE_ERROR: (
176
+ "PostgreSQL mark stale operation failed"
177
+ ),
178
+ EnumPostgresErrorCode.HEARTBEAT_ERROR: "PostgreSQL heartbeat update failed",
179
+ EnumPostgresErrorCode.DEACTIVATE_ERROR: "PostgreSQL deactivation failed",
180
+ EnumPostgresErrorCode.CLEANUP_ERROR: "PostgreSQL topic cleanup failed",
181
+ EnumPostgresErrorCode.UNKNOWN_ERROR: (
182
+ "Unknown error during PostgreSQL operation"
183
+ ),
184
+ }
185
+ return descriptions.get(self, "Unknown error")
186
+
187
+
188
+ __all__ = ["EnumPostgresErrorCode"]
@@ -50,6 +50,7 @@ import asyncpg
50
50
 
51
51
  from omnibase_core.container import ModelONEXContainer
52
52
  from omnibase_core.enums.enum_node_kind import EnumNodeKind
53
+ from omnibase_core.models.primitives.model_semver import ModelSemVer
53
54
  from omnibase_infra.enums import EnumInfraTransportType
54
55
  from omnibase_infra.errors import (
55
56
  InfraConnectionError,
@@ -87,6 +88,9 @@ DEFAULT_POOL_SIZE = 10
87
88
  DEFAULT_TIMEOUT_SECONDS = 30.0
88
89
 
89
90
  # SQL statements
91
+ # NOTE: Database column is `registered_at` but model uses `created_at`. The column
92
+ # is aliased in queries for mapping. This aligns with the existing database schema
93
+ # on 192.168.86.200 which uses `registered_at` for the creation timestamp.
90
94
  SQL_CREATE_TABLE = """
91
95
  CREATE TABLE IF NOT EXISTS node_registrations (
92
96
  node_id UUID PRIMARY KEY,
@@ -95,13 +99,13 @@ CREATE TABLE IF NOT EXISTS node_registrations (
95
99
  capabilities JSONB NOT NULL DEFAULT '[]',
96
100
  endpoints JSONB NOT NULL DEFAULT '{}',
97
101
  metadata JSONB NOT NULL DEFAULT '{}',
98
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
102
+ registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
99
103
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
100
104
  );
101
105
  """
102
106
 
103
107
  SQL_UPSERT = """
104
- INSERT INTO node_registrations (node_id, node_type, node_version, capabilities, endpoints, metadata, created_at, updated_at)
108
+ INSERT INTO node_registrations (node_id, node_type, node_version, capabilities, endpoints, metadata, registered_at, updated_at)
105
109
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
106
110
  ON CONFLICT (node_id) DO UPDATE SET
107
111
  node_type = EXCLUDED.node_type,
@@ -114,7 +118,7 @@ RETURNING (xmax = 0) AS was_insert;
114
118
  """
115
119
 
116
120
  SQL_QUERY_BASE = """
117
- SELECT node_id, node_type, node_version, capabilities, endpoints, metadata, created_at, updated_at
121
+ SELECT node_id, node_type, node_version, capabilities, endpoints, metadata, registered_at AS created_at, updated_at
118
122
  FROM node_registrations
119
123
  """
120
124
 
@@ -384,9 +388,9 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
384
388
  result = await asyncio.wait_for(
385
389
  conn.fetchrow(
386
390
  SQL_UPSERT,
387
- record.node_id,
391
+ str(record.node_id), # VARCHAR column requires string
388
392
  record.node_type.value,
389
- record.node_version,
393
+ str(record.node_version), # VARCHAR column requires string
390
394
  capabilities_json,
391
395
  endpoints_json,
392
396
  metadata_json,
@@ -502,7 +506,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
502
506
  # Filter by node_id if specified (exact match)
503
507
  if query.node_id is not None:
504
508
  conditions.append(f"node_id = ${param_idx}")
505
- params.append(query.node_id)
509
+ params.append(str(query.node_id)) # VARCHAR column requires string
506
510
  param_idx += 1
507
511
 
508
512
  # Filter by node_type if specified
@@ -531,15 +535,15 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
531
535
  count_params = params[:-2] # Exclude limit and offset
532
536
 
533
537
  async with pool.acquire() as conn:
534
- rows, count_result = await asyncio.gather(
535
- asyncio.wait_for(
536
- conn.fetch(sql_query, *params),
537
- timeout=self._timeout_seconds,
538
- ),
539
- asyncio.wait_for(
540
- conn.fetchval(count_query, *count_params),
541
- timeout=self._timeout_seconds,
542
- ),
538
+ # NOTE: asyncpg connections don't support concurrent operations,
539
+ # so we run these queries sequentially instead of with asyncio.gather
540
+ rows = await asyncio.wait_for(
541
+ conn.fetch(sql_query, *params),
542
+ timeout=self._timeout_seconds,
543
+ )
544
+ count_result = await asyncio.wait_for(
545
+ conn.fetchval(count_query, *count_params),
546
+ timeout=self._timeout_seconds,
543
547
  )
544
548
 
545
549
  # Reset circuit breaker on success
@@ -555,11 +559,14 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
555
559
  endpoints = json.loads(row["endpoints"]) if row["endpoints"] else {}
556
560
  metadata = json.loads(row["metadata"]) if row["metadata"] else {}
557
561
 
562
+ # Convert database types to model types:
563
+ # - node_id: VARCHAR -> UUID
564
+ # - node_version: VARCHAR -> ModelSemVer
558
565
  records.append(
559
566
  ModelRegistrationRecord(
560
- node_id=row["node_id"],
567
+ node_id=UUID(row["node_id"]),
561
568
  node_type=EnumNodeKind(row["node_type"]),
562
- node_version=row["node_version"],
569
+ node_version=ModelSemVer.parse(row["node_version"]),
563
570
  capabilities=capabilities,
564
571
  endpoints=endpoints,
565
572
  metadata=metadata,
@@ -684,11 +691,11 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
684
691
  result = await asyncio.wait_for(
685
692
  conn.fetchval(
686
693
  SQL_UPDATE,
687
- node_id,
694
+ str(node_id), # VARCHAR column requires string
688
695
  capabilities_json,
689
696
  endpoints_json,
690
697
  metadata_json,
691
- node_version,
698
+ str(node_version) if node_version is not None else None,
692
699
  ),
693
700
  timeout=self._timeout_seconds,
694
701
  )
@@ -792,7 +799,9 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
792
799
 
793
800
  async with pool.acquire() as conn:
794
801
  result = await asyncio.wait_for(
795
- conn.fetchval(SQL_DELETE, node_id),
802
+ conn.fetchval(
803
+ SQL_DELETE, str(node_id)
804
+ ), # VARCHAR column requires string
796
805
  timeout=self._timeout_seconds,
797
806
  )
798
807
 
@@ -7,6 +7,7 @@ Reusable mixin classes providing:
7
7
  - Infrastructure error integration
8
8
  - Correlation ID propagation
9
9
  - Configurable behavior
10
+ - PostgreSQL error response building for effect persistence
10
11
 
11
12
  Exports (in __all__):
12
13
  Mixins:
@@ -14,8 +15,13 @@ Exports (in __all__):
14
15
  - MixinDictLikeAccessors: Dictionary-style access helpers
15
16
  - MixinEnvelopeExtraction: Event envelope extraction utilities
16
17
  - MixinNodeIntrospection: Node capability introspection
18
+ - MixinPostgresErrorResponse: PostgreSQL exception handling for persistence
19
+ - MixinPostgresOpExecutor: PostgreSQL operation execution with error handling
17
20
  - MixinRetryExecution: Retry logic with exponential backoff
18
21
 
22
+ Dataclasses:
23
+ - PostgresErrorContext: Context for PostgreSQL error handling
24
+
19
25
  Protocols (co-located with their tightly-coupled mixins):
20
26
  - ProtocolCircuitBreakerAware: Interface for circuit breaker capability.
21
27
  Co-located here because it is tightly coupled to MixinAsyncCircuitBreaker.
@@ -48,6 +54,11 @@ from omnibase_infra.mixins.mixin_async_circuit_breaker import MixinAsyncCircuitB
48
54
  from omnibase_infra.mixins.mixin_dict_like_accessors import MixinDictLikeAccessors
49
55
  from omnibase_infra.mixins.mixin_envelope_extraction import MixinEnvelopeExtraction
50
56
  from omnibase_infra.mixins.mixin_node_introspection import MixinNodeIntrospection
57
+ from omnibase_infra.mixins.mixin_postgres_error_response import (
58
+ MixinPostgresErrorResponse,
59
+ PostgresErrorContext,
60
+ )
61
+ from omnibase_infra.mixins.mixin_postgres_op_executor import MixinPostgresOpExecutor
51
62
  from omnibase_infra.mixins.mixin_retry_execution import MixinRetryExecution
52
63
  from omnibase_infra.mixins.protocol_circuit_breaker_aware import (
53
64
  ProtocolCircuitBreakerAware,
@@ -63,6 +74,9 @@ __all__: list[str] = [
63
74
  "MixinDictLikeAccessors",
64
75
  "MixinEnvelopeExtraction",
65
76
  "MixinNodeIntrospection",
77
+ "MixinPostgresErrorResponse",
78
+ "MixinPostgresOpExecutor",
79
+ "PostgresErrorContext",
66
80
  "MixinRetryExecution",
67
81
  "ModelCircuitBreakerConfig",
68
82
  "ModelRetryErrorClassification",