openframe-core 3.7.0__tar.gz → 3.8.0__tar.gz

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 (161) hide show
  1. {openframe_core-3.7.0 → openframe_core-3.8.0}/.github/CHANGELOG.md +34 -0
  2. {openframe_core-3.7.0 → openframe_core-3.8.0}/PKG-INFO +1 -1
  3. openframe_core-3.8.0/docs/technical/architecture/adrs/adr-008-session-handler.md +113 -0
  4. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/system-architecture.md +1 -1
  5. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/overview/roadmap.md +101 -6
  6. {openframe_core-3.7.0 → openframe_core-3.8.0}/mkdocs.yml +1 -0
  7. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/inbound/__init__.py +9 -0
  8. openframe_core-3.8.0/openframe/core/inbound/session.py +180 -0
  9. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/__init__.py +9 -0
  10. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/__init__.py +7 -4
  11. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/inbound.py +94 -9
  12. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/fakes/__init__.py +7 -4
  13. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/fakes/inbound.py +80 -7
  14. {openframe_core-3.7.0 → openframe_core-3.8.0}/pyproject.toml +1 -1
  15. openframe_core-3.8.0/tests/test_inbound.py +296 -0
  16. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_inbound_contracts.py +86 -1
  17. openframe_core-3.8.0/tests/test_remote.py +66 -0
  18. openframe_core-3.7.0/tests/test_inbound.py +0 -158
  19. {openframe_core-3.7.0 → openframe_core-3.8.0}/.github/requirements/docs.txt +0 -0
  20. {openframe_core-3.7.0 → openframe_core-3.8.0}/.github/requirements/test.txt +0 -0
  21. {openframe_core-3.7.0 → openframe_core-3.8.0}/.github/workflows/app-test.yml +0 -0
  22. {openframe_core-3.7.0 → openframe_core-3.8.0}/.github/workflows/auto-docs.yml +0 -0
  23. {openframe_core-3.7.0 → openframe_core-3.8.0}/.github/workflows/python-build.yml +0 -0
  24. {openframe_core-3.7.0 → openframe_core-3.8.0}/.gitignore +0 -0
  25. {openframe_core-3.7.0 → openframe_core-3.8.0}/.vscode/settings.json +0 -0
  26. {openframe_core-3.7.0 → openframe_core-3.8.0}/LICENSE +0 -0
  27. {openframe_core-3.7.0 → openframe_core-3.8.0}/README.md +0 -0
  28. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/assets/images/banner.png +0 -0
  29. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/assets/images/favicon.png +0 -0
  30. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/assets/images/logo-dark.png +0 -0
  31. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/assets/images/logo-light.png +0 -0
  32. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/assets/javascripts/extra.js +0 -0
  33. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/assets/stylesheets/extra.css +0 -0
  34. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/cicd/pipeline.md +0 -0
  35. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/getting-started/index.md +0 -0
  36. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/config.md +0 -0
  37. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/exceptions.md +0 -0
  38. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/health.md +0 -0
  39. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/inbound.md +0 -0
  40. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/index.md +0 -0
  41. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/middleware.md +0 -0
  42. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/plugins.md +0 -0
  43. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/ports.md +0 -0
  44. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/runtime.md +0 -0
  45. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/telemetry.md +0 -0
  46. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/testing.md +0 -0
  47. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/code/modules/tracing.md +0 -0
  48. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/developer-guide/debugging.md +0 -0
  49. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/developer-guide/explore.md +0 -0
  50. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/developer-guide/first-change.md +0 -0
  51. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/developer-guide/how-it-works.md +0 -0
  52. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/developer-guide/local-dev.md +0 -0
  53. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/developer-guide/quick-start/index.md +0 -0
  54. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/index.md +0 -0
  55. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
  56. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
  57. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
  58. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
  59. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
  60. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
  61. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/adrs/adr-007-schema-contracts.md +0 -0
  62. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/adrs/index.md +0 -0
  63. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/capability-taxonomy.md +0 -0
  64. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/component-lifecycle.md +0 -0
  65. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/data-flows.md +0 -0
  66. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/design-decisions.md +0 -0
  67. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/error-taxonomy.md +0 -0
  68. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
  69. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
  70. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/package-journey/index.md +0 -0
  71. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
  72. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
  73. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/deployment/cicd.md +0 -0
  74. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/deployment/environments.md +0 -0
  75. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/deployment/secrets-and-config.md +0 -0
  76. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/infrastructure/networking.md +0 -0
  77. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/infrastructure/platform.md +0 -0
  78. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/infrastructure/storage.md +0 -0
  79. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/operations/index.md +0 -0
  80. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/operations/monitoring.md +0 -0
  81. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
  82. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
  83. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
  84. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
  85. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
  86. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
  87. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/overview/glossary.md +0 -0
  88. {openframe_core-3.7.0 → openframe_core-3.8.0}/docs/technical/overview/system-overview.md +0 -0
  89. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/__init__.py +0 -0
  90. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/__init__.py +0 -0
  91. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/config/__init__.py +0 -0
  92. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/config/settings.py +0 -0
  93. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/exceptions/__init__.py +0 -0
  94. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/exceptions/adapter.py +0 -0
  95. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/exceptions/base.py +0 -0
  96. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/exceptions/codes.py +0 -0
  97. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/exceptions/plugin.py +0 -0
  98. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/inbound/command.py +0 -0
  99. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/inbound/context.py +0 -0
  100. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/inbound/query.py +0 -0
  101. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/inbound/usecase.py +0 -0
  102. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/middleware/__init__.py +0 -0
  103. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/middleware/telemetry.py +0 -0
  104. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/middleware/types.py +0 -0
  105. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/plugins/__init__.py +0 -0
  106. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/plugins/registry.py +0 -0
  107. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/__init__.py +0 -0
  108. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/capability.py +0 -0
  109. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/context.py +0 -0
  110. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/health.py +0 -0
  111. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/identity.py +0 -0
  112. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/lifecycle.py +0 -0
  113. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/outbound/__init__.py +0 -0
  114. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/outbound/consumer.py +0 -0
  115. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/outbound/producer.py +0 -0
  116. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/outbound/repository.py +0 -0
  117. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/ports/port.py +0 -0
  118. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/remote/__init__.py +0 -0
  119. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/remote/client.py +0 -0
  120. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/resilience/__init__.py +0 -0
  121. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/resilience/circuit_breaker.py +0 -0
  122. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/runtime/__init__.py +0 -0
  123. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/runtime/bootstrap.py +0 -0
  124. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/schemas/__init__.py +0 -0
  125. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/telemetry/__init__.py +0 -0
  126. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/telemetry/setup.py +0 -0
  127. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/consumer.py +0 -0
  128. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/graph_store.py +0 -0
  129. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/lifecycle.py +0 -0
  130. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/port.py +0 -0
  131. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/producer.py +0 -0
  132. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/remote.py +0 -0
  133. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/repository.py +0 -0
  134. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/contracts/vector_store.py +0 -0
  135. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/fakes/consumer.py +0 -0
  136. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/fakes/graph_store.py +0 -0
  137. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/fakes/producer.py +0 -0
  138. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/fakes/repository.py +0 -0
  139. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/fakes/vector_store.py +0 -0
  140. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/testing/fixtures.py +0 -0
  141. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/tracing/__init__.py +0 -0
  142. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/tracing/propagation.py +0 -0
  143. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/core/tracing/proxy.py +0 -0
  144. {openframe_core-3.7.0 → openframe_core-3.8.0}/openframe/py.typed +0 -0
  145. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/conftest.py +0 -0
  146. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_config.py +0 -0
  147. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_exceptions.py +0 -0
  148. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_middleware.py +0 -0
  149. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_middleware_types.py +0 -0
  150. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_plugins.py +0 -0
  151. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_ports.py +0 -0
  152. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_ports_primitives.py +0 -0
  153. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_record_error.py +0 -0
  154. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_resilience.py +0 -0
  155. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_runtime.py +0 -0
  156. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_schemas.py +0 -0
  157. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_shutdown_telemetry.py +0 -0
  158. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_telemetry_sidecar.py +0 -0
  159. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_testing_fakes.py +0 -0
  160. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_tracing.py +0 -0
  161. {openframe_core-3.7.0 → openframe_core-3.8.0}/tests/test_tracing_propagation.py +0 -0
@@ -11,6 +11,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ---
13
13
 
14
+ ## v3.8.0 — 2026-10-17
15
+
16
+ ### Added — `SessionHandler`/`Session`/`CloseReason` (ADR-008)
17
+
18
+ A new driving-side contract for long-lived connections (WebSocket, SSE),
19
+ sibling to `UseCase`/`CommandHandler`/`QueryHandler` (ADR-006). Built to
20
+ unblock `openframe-protocol`'s session-transport work, which found the
21
+ design spec's own `SessionHandler` sketch had zero code anywhere and left
22
+ one concrete question unresolved: how a handler sends events *out*.
23
+
24
+ - `openframe.core.inbound.Session[TOut]` — `@runtime_checkable Protocol`,
25
+ `context: RequestContext` (built once per session, not once per event)
26
+ plus `send(event)`/`close(reason)`. Push-based, not a pull-based
27
+ `AsyncIterator` return value — see ADR-008 for the full reasoning.
28
+ - `openframe.core.inbound.SessionHandler[TIn, TOut]` —
29
+ `on_open(session)`/`on_event(session, event)`/`on_close(session, reason)`.
30
+ `on_open` doubles as accept/reject: raising rejects, flowing through the
31
+ same `categorize`/`build_envelope` path (`openframe-protocol-base`)
32
+ every other inbound contract's failures already use.
33
+ - `openframe.core.inbound.CloseReason` — closed `Enum`:
34
+ `COMPLETED`/`CLIENT_DISCONNECT`/`SERVER_SHUTDOWN`/`ERROR`.
35
+ - `openframe.core.testing.SessionHandlerContractTests` and
36
+ `openframe.core.testing.InMemorySession` — same fixture-based subclass
37
+ pattern as the three existing inbound contract-test classes/fakes.
38
+ - Deliberately built directly in `openframe-core`, not incubated in
39
+ `openframe-protocol-base` first per the design spec's own stated
40
+ rule-of-three gate — documented as an explicit, conscious exception in
41
+ ADR-008, not a precedent for skipping that discipline generally.
42
+ - Purely additive — no existing contract changed. 445 tests passing (see
43
+ `tests/test_inbound.py`/`test_inbound_contracts.py` for the new
44
+ `SessionHandler`/`Session`/`InMemorySession` coverage).
45
+
46
+ ---
47
+
14
48
  ## v3.7.0 — 2026-10-11
15
49
 
16
50
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: openframe-core
3
- Version: 3.7.0
3
+ Version: 3.8.0
4
4
  Summary: OpenFrame Microservice Suite - platform kernel. Port contracts, testing utilities, plugin registry, and composition root.
5
5
  Project-URL: Homepage, https://github.com/Furious-Meteors/openframe-core
6
6
  Project-URL: Documentation, https://furious-meteors.github.io/openframe-core/
@@ -0,0 +1,113 @@
1
+ # ADR-008 — `SessionHandler`: a driving-side contract for long-lived connections
2
+
3
+ **Status:** Accepted — v3.8.0
4
+ **Extends:** [ADR-006 — Unified Port + Lifecycle Contract](adr-006-unified-port-lifecycle.md)'s
5
+ `openframe.core.inbound` module (`UseCase`, `CommandHandler`, `QueryHandler`, `RequestContext`).
6
+
7
+ ---
8
+
9
+ ## Context
10
+
11
+ `openframe-protocol` builds two request/response transports (`-http`, `-grpc`) on
12
+ `openframe.core.inbound`'s existing contracts: one input in, one output out, done. Neither shape
13
+ fits a long-lived connection — WebSocket (bidirectional) or Server-Sent Events (unidirectional,
14
+ server→client) — where a handler may receive zero or many inbound events and send zero or many
15
+ outbound events over the connection's lifetime.
16
+
17
+ `openframe-protocol`'s own design spec sketches a `SessionHandler[TEvent]` contract
18
+ (`on_open`/`on_event`/`on_close`) as the intended fix, with an explicit rule: incubate it in
19
+ `openframe-protocol-base` first, and only promote it to `openframe-core` once three independent
20
+ session transports have proven the shape against the same conformance suite — the same
21
+ rule-of-three discipline this repo already applies to `Capability` and `ErrorCategory` additions.
22
+
23
+ That sketch has zero lines of code anywhere, in either repo, and the design spec itself flags one
24
+ concrete unresolved question as a blocker: **how does a handler actually send events to the
25
+ client?** `on_open`/`on_event`/`on_close` describe the inbound (driving) half of the contract only
26
+ — receiving events and lifecycle notifications — but say nothing about the outbound half.
27
+
28
+ ## Decision
29
+
30
+ **Build `SessionHandler` directly in `openframe.core.inbound`, not incubated in
31
+ `openframe-protocol-base` first** — a deliberate deviation from the stated rule-of-three gate (see
32
+ "Why skip incubation" below) — alongside a new `Session[TOut]` contract that resolves the
33
+ unanswered outbound-events question.
34
+
35
+ - **`Session[TOut]`** (`openframe/core/inbound/session.py`) — `@runtime_checkable Protocol`,
36
+ carries `context: RequestContext` (built once per session, not once per event — a session has
37
+ one correlation id for its whole lifetime) plus two methods:
38
+ - `async def send(self, event: TOut) -> None` — push one outbound event.
39
+ - `async def close(self, reason: CloseReason = CloseReason.COMPLETED) -> None` — end the
40
+ session. Idempotent.
41
+
42
+ **Push-based, not a pull-based `AsyncIterator[TOut]` return value.** A handler can call
43
+ `session.send()` from inside `on_event` (responding to an inbound message) *or* from a
44
+ background task it spawns at `on_open` (e.g. forwarding events from an unrelated source — a
45
+ Kafka consumer — into an already-open connection). A generator return value can't express the
46
+ latter without the handler building its own internal queue anyway, at which point that queue
47
+ *is* a `Session` object in a different shape — so `Session` names the thing directly instead of
48
+ leaving every handler to reinvent it.
49
+
50
+ - **`SessionHandler[TIn, TOut]`** (same module) — `@runtime_checkable Protocol` with three methods:
51
+ - `async def on_open(self, session: Session[TOut]) -> None` — accept (return normally) or reject
52
+ (raise) a new session. A raised exception flows through
53
+ `openframe.protocol.base.errors.categorize`/`build_envelope` — the exact same mapping path
54
+ every other inbound contract's failures already use — rather than a second return-value
55
+ channel a caller would need a separate check for.
56
+ - `async def on_event(self, session: Session[TOut], event: TIn) -> None` — called once per
57
+ inbound event, in arrival order. **Never called at all** for a one-directional transport (SSE,
58
+ which has no client→server events) — a valid, expected lifecycle, not a contract violation.
59
+ - `async def on_close(self, session: Session[TOut], reason: CloseReason) -> None` — called
60
+ exactly once, however the session ended.
61
+
62
+ - **`CloseReason`** (same module) — closed `Enum`: `COMPLETED`, `CLIENT_DISCONNECT`,
63
+ `SERVER_SHUTDOWN`, `ERROR`. Every session transport maps its own disconnect/shutdown signal onto
64
+ one of these four, mirroring how `ErrorCategory` gives every transport one shared vocabulary for
65
+ errors instead of each inventing its own.
66
+
67
+ - **Testing**: `SessionHandlerContractTests` (`testing/contracts/inbound.py`, alongside the
68
+ existing three inbound contract-test classes) and `InMemorySession`
69
+ (`testing/fakes/inbound.py`, alongside `EchoUseCase`/`SpyCommandHandler`/`SpyQueryHandler`) —
70
+ same fixture-based subclass pattern, same zero-pytest-at-module-level discipline.
71
+
72
+ ### Why skip incubation in `-base`
73
+
74
+ The rule-of-three gate exists to de-risk a contract shape against real, demonstrated need before
75
+ it lands in the platform kernel. The one genuine open question that gate would have surfaced —
76
+ how outbound events are sent — is resolved by this decision itself (the `Session` push model
77
+ above), reasoned from first principles against both a one-directional transport (SSE) and a
78
+ bidirectional one (WebSocket) rather than discovered incrementally across three separate builds.
79
+ Incubating a second time in `-base` before any transport exists to prove it against would add a
80
+ layer of indirection with no remaining uncertainty left to burn down. This is recorded here as an
81
+ explicit, conscious exception — not a precedent for skipping the rule-of-three discipline
82
+ generally; `Capability`/`ErrorCategory`-style additions should still default to it.
83
+
84
+ ## Consequences
85
+
86
+ - **`openframe.core.inbound.__init__`** exports `SessionHandler`, `Session`, `CloseReason`
87
+ alongside the existing `UseCase`, `CommandHandler`, `QueryHandler`, `RequestContext`.
88
+ - **No change to any existing contract.** `UseCase`/`CommandHandler`/`QueryHandler`/
89
+ `RequestContext` are untouched — this is a pure addition.
90
+ - **`openframe-protocol-base` gains transport-neutral session plumbing next** (a `run_session`
91
+ loop helper and a `SessionConformanceTests` suite), reusing `build_request_context` and
92
+ `categorize`/`build_envelope` as-is — both already generic enough for a session's single
93
+ per-connection context and in-band error events, confirmed during design research, not assumed.
94
+ - **SSE is the first transport to implement `SessionHandler`**, as a new module inside
95
+ `openframe-protocol-http` (per that repo's own design spec placement decision) rather than a new
96
+ package — chosen because it is one-directional (`on_event` never fires), the lower-risk half of
97
+ the contract to prove first. WebSocket (bidirectional, its own package-boundary question) is
98
+ explicitly out of scope for this ADR.
99
+ - **Version bump: `3.7.0` → `3.8.0`** — additive, non-breaking minor version.
100
+
101
+ ## Alternatives considered
102
+
103
+ - **Pull-based `AsyncIterator[TOut]` return value from `on_open`.** Rejected: maps cleanly onto
104
+ SSE alone, but can't express a handler pushing an event from outside its own generator's loop
105
+ (an external task, or `on_event` triggering an unprompted send) without the handler building an
106
+ internal queue regardless — see "Decision" above.
107
+ - **Model session rejection as a return value** (e.g. `on_open` returns `bool`) instead of raising.
108
+ Rejected: every other inbound contract in this module signals failure by raising, and a second,
109
+ bespoke "did it accept" channel here would mean `SessionHandler` is the only inbound contract a
110
+ caller can't route through the shared `categorize`/`build_envelope` error path uniformly.
111
+ - **Incubate in `openframe-protocol-base` first, per the design spec's own stated rule.**
112
+ Rejected for this specific contract — see "Why skip incubation" above. Not a general reversal of
113
+ the rule-of-three discipline for future additions.
@@ -11,7 +11,7 @@ As of v3.1.0, `ports/` unifies what were previously two separate
11
11
  modules: `openframe.core.contracts` (the port/lifecycle primitives) and
12
12
  `openframe.core.ports` (the outbound protocols, now internally organised
13
13
  under `ports/outbound/`). See
14
- [ADR-006's v3.1.0 addendum](adrs/adr-006-unified-port-lifecycle.md#addendum--contracts-merged-into-ports-outbound-sub-module-introduced-v310)
14
+ [ADR-006's v3.1.0 addendum](adrs/adr-006-unified-port-lifecycle.md#addendum-contracts-merged-into-ports-outbound-sub-module-introduced-v310)
15
15
  for the full rationale.
16
16
 
17
17
  ---
@@ -41,11 +41,106 @@ Lower-priority items:
41
41
 
42
42
  ### openframe-adapters
43
43
 
44
- - `openframe-adapters-db-postgres` — first adapter, reference implementation
45
- - `openframe-adapters-db-redis` — key-value, pub/sub, semantic cache
46
- - `openframe-adapters-db-mongo` — document store for AI Research Vault Phase 1
47
- - `openframe-adapters-queue-kafka` — producer + consumer, unlocks event-driven template
48
- - Remaining 11 adapters in parallel once the pattern is validated
44
+ Done since this was last updated:
45
+
46
+ - ~~`openframe-adapters-db-postgres` — first adapter, reference implementation~~
47
+ - ~~`openframe-adapters-db-redis` — key-value, pub/sub, semantic cache~~
48
+ - ~~`openframe-adapters-db-mongo` — document store for AI Research Vault Phase 1~~
49
+ - ~~`openframe-adapters-queue-kafka` — producer + consumer, unlocks event-driven template~~
50
+ - ~~Remaining 11 adapters in parallel once the pattern is validated~~ —
51
+ built in checklist-driven waves (see `openframe-adapters`' own
52
+ `.github/CHANGELOG.md`): queue (nats, rabbitmq), relational
53
+ (cockroachdb, mysql, mariadb, oracle), key-value/columnar (dynamodb,
54
+ cassandra), time-series (influxdb), vector (qdrant, milvus, chromadb),
55
+ graph (falkordb). 17 real packages total. FAISS remains explicitly
56
+ descoped — no native id→metadata store, doesn't fit
57
+ `BaseVectorStore`'s id-addressable design.
58
+ - ~~`BaseVectorStore[T]` / `BaseGraphStore[T]`~~ — both shipped in this
59
+ repo (v3.5.0 / v3.6.0), each extending `BaseRepository[T]` directly
60
+ plus exactly one new method, rather than as disjoint sibling ports.
61
+ See [ADR-006](../architecture/adrs/adr-006-unified-port-lifecycle.md)
62
+ for the port-design philosophy both followed.
63
+
64
+ Next capabilities under consideration — each is a candidate NEW
65
+ top-level capability, not an extension of an existing adapter package.
66
+ **Every adapter in this ecosystem depends only on `openframe-core` plus
67
+ its own driver library, never on a sibling `openframe-adapters-*`
68
+ package** (e.g. `openframe-adapters-db-falkordb` depends on `redis-py`
69
+ directly, not on `openframe-adapters-db-redis`, even though FalkorDB is
70
+ Redis-based under the hood) — this constraint applies to every item
71
+ below too, most pointedly the Redis-backed ones:
72
+
73
+ - **Object storage** (`BaseObjectStore` — already named in the v3.1.0
74
+ entry further down this same file) — S3, GCS, Azure Blob, MinIO. Strongest fit for the existing
75
+ extend-`BaseRepository`-plus-one-method pattern; put/get/delete/list
76
+ by key is arguably more naturally CRUD-shaped than graph nodes were.
77
+ **Superseded twice, history kept for continuity.** Pass 1 of this
78
+ entry proposed an outbound FTP/SFTP client adapter here under
79
+ `Capability.STORAGE` (server half → `openframe-protocol`, client half
80
+ → here). Pass 2 (`openframe-protocol` draft 7) moved both halves of
81
+ FTP into `openframe-protocol-ftp` together, removing it as a
82
+ candidate here entirely. **Pass 3 (`openframe-protocol` draft 8,
83
+ current)** reverses pass 2: the boundary is now "by peer, not by
84
+ protocol" — FTP's *server* half (accepting uploads from unknown
85
+ callers) stays in `openframe-protocol-ftp`, but the *client* half
86
+ (connecting to one specific named third-party FTP server) is a
87
+ third-party-peer integration and belongs back here, in
88
+ `openframe-adapters`, as originally thought. Its exact `Capability`
89
+ fit is explicitly **not yet decided** (draft 8's own open decision
90
+ D11 — `STORAGE` is a candidate, not settled) — don't treat the old
91
+ pass-1 `STORAGE` placement as confirmed; it's a plausible answer to
92
+ an open question, not a closed one. Same reopened status applies to
93
+ RTMP push and RTSP pull (D11 again — none of the eleven existing
94
+ `Capability` members fits either yet) and to a raw SMTP relay
95
+ (tentatively `NOTIFICATION`, same not-yet-added caveat as below).
96
+ - **Full-text/structured search** — Elasticsearch, OpenSearch. Reuses
97
+ `Capability.SEARCH` (already shared with vector/graph stores); own
98
+ query shape (term/boolean queries, not vector similarity or Cypher).
99
+ - **Secrets management** (`BaseSecretsProvider` — also pre-named) —
100
+ Vault, AWS/GCP/Azure secret managers. Not CRUD-shaped — versioned key
101
+ lookups need their own port design, not a `BaseRepository` extension.
102
+ - **Feature flags** (`BaseFeatureFlagProvider` — also pre-named) —
103
+ LaunchDarkly, Unleash, GrowthBook. Not CRUD-shaped — boolean/variant
104
+ evaluation.
105
+ - **Distributed locking** — own Redis/etcd/Zookeeper client (never the
106
+ `openframe-adapters-db-redis` package itself). `acquire`/`release`/
107
+ `extend` is close to CRUD-shaped but isn't quite it.
108
+ - **Notifications** (email/SMS/push, plus Slack/webhook alerts and
109
+ PagerDuty/Opsgenie-style paging) — SES, SendGrid, Twilio, FCM, Slack
110
+ incoming webhooks, PagerDuty. Shaped like `BaseProducer`
111
+ (fire-and-forget/ack), not `BaseRepository`. Intended new taxonomy
112
+ member: `Capability.NOTIFICATION` — named now, not added to the enum
113
+ yet (see below). The role this covers is distinct from this repo's
114
+ existing logging/OTel-metrics signals: those are machine-consumed
115
+ telemetry (dashboards, alerting pipelines that process metrics);
116
+ `NOTIFICATION` covers reaching an actual human or external system
117
+ directly (an email landing in an inbox, a page firing, a Slack
118
+ message posting) — a different capability, not a backend for the
119
+ telemetry this repo already has. Candidate justification, following
120
+ the same bar `Capability.SEARCH` met (shared by multiple genuinely
121
+ analogous backends, not invented for one): email, SMS, push, and
122
+ chat/paging providers are all the same operational role — deliver a
123
+ message to an end-human/device via an external provider — which is
124
+ why this is a stronger fit than forcing it onto the existing `QUEUE`
125
+ member (which specifically describes message *brokers* with
126
+ consumer-side subscribe/ack semantics; SES/SendGrid/Slack have none
127
+ of that).
128
+ - **Do not add `Capability.NOTIFICATION` to the enum speculatively.**
129
+ Per this repo's own closed-taxonomy discipline (ADR-006;
130
+ `Capability.TRANSPORT` was not repurposed for inbound transport
131
+ servers in the `openframe-protocol` design review for the same
132
+ reason) — add the member only once a real adapter (email, SMS,
133
+ push, or paging) is actually being built, not ahead of one. This
134
+ bullet records the intended name and justification so that moment
135
+ doesn't have to re-derive either from scratch.
136
+ - **Identity/auth verification** — Auth0, Okta, Keycloak token
137
+ introspection or user lookup.
138
+ - **Rate limiting** — same Redis-driver-directly caveat as locking.
139
+
140
+ Deliberately out of scope for now: workflow orchestration (Temporal/
141
+ Camunda — too large a port surface for one method), schema registries
142
+ (Kafka-ecosystem-specific, niche), payment processing (too
143
+ domain-specific for generic infra adapters).
49
144
 
50
145
  ### openframe-suite
51
146
 
@@ -65,7 +160,7 @@ Lower-priority items:
65
160
  - `ApplicationBootstrap.registry` — new read-only property exposing the underlying `PluginRegistry`.
66
161
  - `PluginRegistry(default_init_timeout=...)` / `register(..., init_timeout=...)` (mirrored on `ApplicationBootstrap`) — bound how long `initialize_all()` waits for a port's `initialize()`. Both default to `None` (no timeout) — fully backward-compatible.
67
162
  - Fixed: `TracingProxy`'s `asyncio.iscoroutinefunction` deprecation; `AmbiguousCapabilityError` now mirrors `capability`/`matches` into `context`.
68
- - Full changelog: [`.github/CHANGELOG.md`](../../../.github/CHANGELOG.md#v330--2026-09-28).
163
+ - Full changelog: [`.github/CHANGELOG.md`](https://github.com/Furious-Meteors/openframe-core/blob/production/.github/CHANGELOG.md#v330--2026-09-28).
69
164
 
70
165
  ---
71
166
 
@@ -156,6 +156,7 @@ nav:
156
156
  - ADR-004 Pydantic Settings: technical/architecture/adrs/adr-004-pydantic.md
157
157
  - ADR-005 Middleware Telemetry: technical/architecture/adrs/adr-005-middleware.md
158
158
  - ADR-006 Unified Port Lifecycle: technical/architecture/adrs/adr-006-unified-port-lifecycle.md
159
+ - ADR-007 Schema Contracts: technical/architecture/adrs/adr-007-schema-contracts.md
159
160
  - Capability Taxonomy: technical/architecture/capability-taxonomy.md
160
161
  - Error Taxonomy: technical/architecture/error-taxonomy.md
161
162
  - Design Decisions: technical/architecture/design-decisions.md
@@ -11,6 +11,11 @@ routes, message-handler entrypoints, CLI commands).
11
11
  - :class:`~openframe.core.inbound.usecase.UseCase` — general driving contract.
12
12
  - :class:`~openframe.core.inbound.command.CommandHandler` — write-side CQRS specialisation.
13
13
  - :class:`~openframe.core.inbound.query.QueryHandler` — read-side CQRS specialisation.
14
+ - :class:`~openframe.core.inbound.session.SessionHandler` — long-lived-connection
15
+ specialisation (ADR-008) for transports where a handler may receive zero or
16
+ many inbound events and send zero or many outbound events over time
17
+ (WebSocket, SSE), via a push-based :class:`~openframe.core.inbound.session.Session`
18
+ handle.
14
19
  - :class:`~openframe.core.inbound.context.RequestContext` — correlation id
15
20
  + optional identity, constructed by inbound adapters and passed into
16
21
  ``execute()``.
@@ -22,11 +27,15 @@ from __future__ import annotations
22
27
  from openframe.core.inbound.command import CommandHandler
23
28
  from openframe.core.inbound.context import RequestContext
24
29
  from openframe.core.inbound.query import QueryHandler
30
+ from openframe.core.inbound.session import CloseReason, Session, SessionHandler
25
31
  from openframe.core.inbound.usecase import UseCase
26
32
 
27
33
  __all__ = [
28
34
  "UseCase",
29
35
  "CommandHandler",
30
36
  "QueryHandler",
37
+ "SessionHandler",
38
+ "Session",
39
+ "CloseReason",
31
40
  "RequestContext",
32
41
  ]
@@ -0,0 +1,180 @@
1
+ """
2
+ openframe/core/inbound/session.py
3
+ ====================================
4
+ SessionHandler — the long-lived-connection driving-side contract
5
+ (ADR-008), sibling to :class:`~openframe.core.inbound.usecase.UseCase`.
6
+
7
+ ``UseCase``/``CommandHandler``/``QueryHandler`` (ADR-006) model one-shot
8
+ request/response exchanges: one input in, one output out, done. They do
9
+ not fit a long-lived connection (WebSocket, SSE) where a handler may
10
+ receive zero or many inbound events and send zero or many outbound
11
+ events over its lifetime. ``SessionHandler`` is that shape.
12
+
13
+ ``Session[TOut]`` is deliberately **push-based**, not a pull-based
14
+ ``AsyncIterator`` return value: ``send()``/``close()`` are available from
15
+ inside ``on_open``, ``on_event``, or a background task the handler spawns
16
+ at ``on_open`` — e.g. forwarding events from an unrelated source (a Kafka
17
+ consumer) into an already-open connection, something a generator can't
18
+ express without building an internal queue anyway, at which point the
19
+ queue *is* a ``Session`` in disguise.
20
+
21
+ ``on_open`` doubles as the accept/reject point — raising rejects the
22
+ session, flowing through the exact same
23
+ ``openframe.protocol.base.errors.categorize``/``build_envelope`` path
24
+ every other inbound contract's failures already do, rather than a
25
+ second return-value channel a caller would need a separate check for.
26
+
27
+ ``RequestContext`` is built once per session (at connection/handshake
28
+ time), not once per event — a session has one correlation id for its
29
+ whole lifetime. This matches ``build_request_context``'s own precondition
30
+ (headers are only available once, at handshake time) for every transport
31
+ this contract is meant to serve.
32
+
33
+ Dependency order:
34
+ inbound/context → ports/context
35
+ inbound/session → inbound/context
36
+ """
37
+ from __future__ import annotations
38
+
39
+ from enum import Enum
40
+ from typing import Protocol, TypeVar, runtime_checkable
41
+
42
+ from openframe.core.inbound.context import RequestContext
43
+
44
+ __all__ = ["CloseReason", "Session", "SessionHandler"]
45
+
46
+ TIn = TypeVar("TIn")
47
+ TOut = TypeVar("TOut")
48
+
49
+
50
+ class CloseReason(Enum):
51
+ """
52
+ Why a session ended.
53
+
54
+ Closed, transport-neutral set — every `openframe-protocol-*` session
55
+ transport maps its own disconnect/shutdown signal onto one of these
56
+ four values rather than inventing its own vocabulary.
57
+ """
58
+
59
+ COMPLETED = "completed"
60
+ """The handler or transport ended the session normally."""
61
+
62
+ CLIENT_DISCONNECT = "client_disconnect"
63
+ """The remote end disconnected (socket closed, stream aborted)."""
64
+
65
+ SERVER_SHUTDOWN = "server_shutdown"
66
+ """The server is shutting down and is closing open sessions."""
67
+
68
+ ERROR = "error"
69
+ """An unhandled exception from `on_open`/`on_event` ended the session."""
70
+
71
+
72
+ @runtime_checkable
73
+ class Session(Protocol[TOut]):
74
+ """
75
+ Handle a :class:`SessionHandler` uses to push events to the remote end
76
+ and to end the session.
77
+
78
+ Adapters satisfy this protocol structurally — no inheritance needed.
79
+ A concrete implementation (e.g. an SSE or WebSocket session) adapts
80
+ its own wire format in :meth:`send`; the handler never sees that
81
+ format.
82
+
83
+ Type parameter:
84
+ TOut: The event type this session sends to the remote end.
85
+
86
+ Attributes:
87
+ context: The session's `RequestContext`, built once at
88
+ connection/handshake time and stable for the session's
89
+ entire lifetime.
90
+ """
91
+
92
+ context: RequestContext
93
+
94
+ async def send(self, event: TOut) -> None:
95
+ """
96
+ Send one event to the remote end.
97
+
98
+ Raises:
99
+ Implementation-defined if called after :meth:`close` — a
100
+ session that has already closed cannot send further events.
101
+ """
102
+ ...
103
+
104
+ async def close(self, reason: CloseReason = CloseReason.COMPLETED) -> None:
105
+ """
106
+ End the session.
107
+
108
+ Idempotent — calling :meth:`close` more than once has no
109
+ additional effect beyond the first call.
110
+
111
+ Args:
112
+ reason: Why the session is ending. Defaults to
113
+ `CloseReason.COMPLETED` — the handler-initiated,
114
+ successful end of the session.
115
+ """
116
+ ...
117
+
118
+
119
+ @runtime_checkable
120
+ class SessionHandler(Protocol[TIn, TOut]):
121
+ """
122
+ Long-lived-connection driving-side contract.
123
+
124
+ Adapters satisfy this protocol structurally — no inheritance needed.
125
+ Any class with matching async `on_open`/`on_event`/`on_close` methods
126
+ satisfies `SessionHandler`.
127
+
128
+ Type parameters:
129
+ TIn: The inbound event type this handler receives via `on_event`.
130
+ TOut: The outbound event type this handler sends via
131
+ `session.send()`.
132
+
133
+ Runtime check::
134
+
135
+ isinstance(handler, SessionHandler) # ✓ works
136
+ isinstance(handler, SessionHandler[str, int]) # ✗ raises TypeError
137
+ """
138
+
139
+ async def on_open(self, session: Session[TOut]) -> None:
140
+ """
141
+ Called once when the session opens.
142
+
143
+ Returning normally accepts the session. Raising rejects it — the
144
+ exception flows through the same
145
+ `openframe.protocol.base.errors.categorize`/`build_envelope` path
146
+ every other inbound contract's failures do, so a reject looks
147
+ like any other mapped error to the caller, not a special case.
148
+
149
+ Args:
150
+ session: The newly opened session. `session.context` carries
151
+ this session's `RequestContext`, stable for its
152
+ whole lifetime.
153
+ """
154
+ ...
155
+
156
+ async def on_event(self, session: Session[TOut], event: TIn) -> None:
157
+ """
158
+ Called once per inbound event, in arrival order.
159
+
160
+ Never called at all for a one-directional transport (e.g. SSE,
161
+ which has no client-to-server events) — this is a valid, expected
162
+ lifecycle for such a handler, not a violation of the contract.
163
+
164
+ Args:
165
+ session: The open session this event arrived on.
166
+ event: The inbound event.
167
+ """
168
+ ...
169
+
170
+ async def on_close(self, session: Session[TOut], reason: CloseReason) -> None:
171
+ """
172
+ Called exactly once when the session ends, however it ended —
173
+ normal completion, client disconnect, server shutdown, or an
174
+ unhandled error from `on_open`/`on_event`.
175
+
176
+ Args:
177
+ session: The now-closed session.
178
+ reason: Why the session ended.
179
+ """
180
+ ...
@@ -29,6 +29,11 @@ Quick reference::
29
29
 
30
30
  use_case = EchoUseCase(response={"id": "1"})
31
31
  assert isinstance(use_case, UseCase)
32
+
33
+ from openframe.core.testing import InMemorySession, SessionHandlerContractTests
34
+
35
+ session = InMemorySession(context=RequestContext(correlation_id="t1"))
36
+ await my_session_handler.on_open(session)
32
37
  """
33
38
  from __future__ import annotations
34
39
 
@@ -37,6 +42,7 @@ from openframe.core.testing.contracts.graph_store import GraphStoreContractTests
37
42
  from openframe.core.testing.contracts.inbound import (
38
43
  CommandHandlerContractTests,
39
44
  QueryHandlerContractTests,
45
+ SessionHandlerContractTests,
40
46
  UseCaseContractTests,
41
47
  )
42
48
  from openframe.core.testing.contracts.lifecycle import LifecycleContractTests
@@ -52,6 +58,7 @@ from openframe.core.testing.fakes.consumer import FakeConsumer
52
58
  from openframe.core.testing.fakes.graph_store import InMemoryGraphStore
53
59
  from openframe.core.testing.fakes.inbound import (
54
60
  EchoUseCase,
61
+ InMemorySession,
55
62
  SpyCommandHandler,
56
63
  SpyQueryHandler,
57
64
  )
@@ -68,6 +75,7 @@ __all__ = [
68
75
  "EchoUseCase",
69
76
  "SpyCommandHandler",
70
77
  "SpyQueryHandler",
78
+ "InMemorySession",
71
79
  "LifecycleContractTests",
72
80
  "PortContractTests",
73
81
  "RepositoryContractTests",
@@ -78,6 +86,7 @@ __all__ = [
78
86
  "UseCaseContractTests",
79
87
  "CommandHandlerContractTests",
80
88
  "QueryHandlerContractTests",
89
+ "SessionHandlerContractTests",
81
90
  "RemoteCommandClientContractTests",
82
91
  "RemoteQueryClientContractTests",
83
92
  ]
@@ -15,10 +15,11 @@ adding traverse-specific assertions — mirroring how ``BaseGraphStore``
15
15
  extends ``BaseRepository``.
16
16
 
17
17
  Inbound: ``UseCaseContractTests``, ``CommandHandlerContractTests``,
18
- ``QueryHandlerContractTests`` mirror the outbound contract classes for
19
- the driving side of the hexagon. They do not build on any shared base
20
- class — ``UseCase``/``CommandHandler``/``QueryHandler`` do not extend
21
- ``BasePort``.
18
+ ``QueryHandlerContractTests``, ``SessionHandlerContractTests`` mirror the
19
+ outbound contract classes for the driving side of the hexagon. They do
20
+ not build on any shared base class —
21
+ ``UseCase``/``CommandHandler``/``QueryHandler``/``SessionHandler`` do not
22
+ extend ``BasePort``.
22
23
 
23
24
  No top-level pytest import — all modules here can be imported without
24
25
  pytest installed.
@@ -32,6 +33,7 @@ from openframe.core.testing.contracts.graph_store import GraphStoreContractTests
32
33
  from openframe.core.testing.contracts.inbound import (
33
34
  CommandHandlerContractTests,
34
35
  QueryHandlerContractTests,
36
+ SessionHandlerContractTests,
35
37
  UseCaseContractTests,
36
38
  )
37
39
  from openframe.core.testing.contracts.lifecycle import LifecycleContractTests
@@ -55,6 +57,7 @@ __all__ = [
55
57
  "UseCaseContractTests",
56
58
  "CommandHandlerContractTests",
57
59
  "QueryHandlerContractTests",
60
+ "SessionHandlerContractTests",
58
61
  "RemoteCommandClientContractTests",
59
62
  "RemoteQueryClientContractTests",
60
63
  ]