reactor-runtime 2.7.8__tar.gz → 2.7.9__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 (139) hide show
  1. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/PKG-INFO +1 -1
  2. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/pyproject.toml +2 -2
  3. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/model_state.py +30 -3
  4. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/runtime_api.py +131 -0
  5. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime.egg-info/PKG-INFO +1 -1
  6. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/README.md +0 -0
  7. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/setup.cfg +0 -0
  8. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/api/__init__.py +0 -0
  9. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/__init__.py +0 -0
  10. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/config.py +0 -0
  11. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/experiment/__init__.py +0 -0
  12. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/experiment/session.py +0 -0
  13. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/__init__.py +0 -0
  14. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/defaults.py +0 -0
  15. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/driver/__init__.py +0 -0
  16. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/driver/pipeline_executor.py +0 -0
  17. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/driver/step_result.py +0 -0
  18. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/events/__init__.py +0 -0
  19. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/events/connected.py +0 -0
  20. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/events/event.py +0 -0
  21. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/events/messages.py +0 -0
  22. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/events/upload.py +0 -0
  23. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/internal/__init__.py +0 -0
  24. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
  25. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/internal/output_buffer.py +0 -0
  26. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/internal/reactor_core.py +0 -0
  27. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/model/__init__.py +0 -0
  28. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/model/decorators.py +0 -0
  29. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/model/handlers.py +0 -0
  30. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
  31. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
  32. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
  33. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
  34. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
  35. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
  36. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
  37. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/tracks/input.py +0 -0
  38. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/tracks/output.py +0 -0
  39. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/interface/upload.py +0 -0
  40. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/__init__.py +0 -0
  41. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/backends/__init__.py +0 -0
  42. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/backends/base.py +0 -0
  43. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/backends/file.py +0 -0
  44. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/backends/otlp.py +0 -0
  45. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/helpers.py +0 -0
  46. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/nvml_sampler.py +0 -0
  47. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/plotting/__init__.py +0 -0
  48. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/plotting/plot_profiling.py +0 -0
  49. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/profiler.py +0 -0
  50. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/singleton.py +0 -0
  51. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/profiling/torch_chunk_profiler.py +0 -0
  52. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/recording/__init__.py +0 -0
  53. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
  54. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/recording/chunk_uploader.py +0 -0
  55. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/recording/config.py +0 -0
  56. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/recording/markers.py +0 -0
  57. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/recording/session_recorder.py +0 -0
  58. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/recording/sinks.py +0 -0
  59. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/recording/track_resolver.py +0 -0
  60. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/runtimes/headless/config.py +0 -0
  61. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/runtimes/headless/headless_runtime.py +0 -0
  62. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/runtimes/headless/input_feeder.py +0 -0
  63. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/runtimes/http/config.py +0 -0
  64. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/runtimes/http/http_runtime.py +0 -0
  65. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/runtimes/http/types.py +0 -0
  66. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/schema.py +0 -0
  67. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/schema_validator.py +0 -0
  68. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/serve/__init__.py +0 -0
  69. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/serve/__main__.py +0 -0
  70. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/serve/commands/__init__.py +0 -0
  71. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/serve/commands/run.py +0 -0
  72. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/serve/commands/schema.py +0 -0
  73. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/serve/main.py +0 -0
  74. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/serve/utils/__init__.py +0 -0
  75. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/serve/utils/config.py +0 -0
  76. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/serve/utils/runtime.py +0 -0
  77. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/__init__.py +0 -0
  78. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/aiortc/__init__.py +0 -0
  79. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/aiortc/audio_track.py +0 -0
  80. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/aiortc/client.py +0 -0
  81. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/aiortc/frame_conversion.py +0 -0
  82. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/aiortc/ice_connection.py +0 -0
  83. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/aiortc/video_track.py +0 -0
  84. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/config.py +0 -0
  85. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/events.py +0 -0
  86. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/__init__.py +0 -0
  87. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/client.py +0 -0
  88. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/decoders/__init__.py +0 -0
  89. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/decoders/av1.py +0 -0
  90. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/decoders/base.py +0 -0
  91. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/decoders/factory.py +0 -0
  92. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/decoders/h264.py +0 -0
  93. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/decoders/h265.py +0 -0
  94. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/decoders/opus.py +0 -0
  95. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/decoders/vp8.py +0 -0
  96. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/decoders/vp9.py +0 -0
  97. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/encoders/__init__.py +0 -0
  98. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/encoders/av1.py +0 -0
  99. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/encoders/base.py +0 -0
  100. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/encoders/factory.py +0 -0
  101. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/encoders/h264.py +0 -0
  102. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/encoders/h265.py +0 -0
  103. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/encoders/opus.py +0 -0
  104. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/encoders/vp8.py +0 -0
  105. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/encoders/vp9.py +0 -0
  106. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/gst.py +0 -0
  107. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/gst_helpers.py +0 -0
  108. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/probes/__init__.py +0 -0
  109. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/probes/fps_probe.py +0 -0
  110. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/receiver/__init__.py +0 -0
  111. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/receiver/audio.py +0 -0
  112. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/receiver/base.py +0 -0
  113. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/receiver/video.py +0 -0
  114. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/sdp/__init__.py +0 -0
  115. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/sdp/bundle.py +0 -0
  116. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/sdp/codec.py +0 -0
  117. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/sdp/extmap.py +0 -0
  118. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/sdp/ice.py +0 -0
  119. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/sender/__init__.py +0 -0
  120. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/sender/audio.py +0 -0
  121. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/sender/base.py +0 -0
  122. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/sender/video.py +0 -0
  123. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/settings.py +0 -0
  124. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/gstreamer/signals.py +0 -0
  125. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/ice_uris.py +0 -0
  126. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/interface.py +0 -0
  127. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/media.py +0 -0
  128. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/transports/types.py +0 -0
  129. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/utils/launch.py +0 -0
  130. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/utils/loader.py +0 -0
  131. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/utils/log.py +0 -0
  132. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/utils/messages.py +0 -0
  133. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/utils/paths.py +0 -0
  134. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/utils/ports.py +0 -0
  135. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime/utils/typing.py +0 -0
  136. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime.egg-info/SOURCES.txt +0 -0
  137. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime.egg-info/dependency_links.txt +0 -0
  138. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime.egg-info/requires.txt +0 -0
  139. {reactor_runtime-2.7.8 → reactor_runtime-2.7.9}/src/reactor_runtime.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor_runtime
3
- Version: 2.7.8
3
+ Version: 2.7.9
4
4
  Summary: Reactor runtime with public model API
5
5
  Author-email: Reactor <team@reactor.inc>
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactor_runtime"
7
- version = "2.7.8"
7
+ version = "2.7.9"
8
8
  description = "Reactor runtime with public model API"
9
9
  authors = [
10
10
  { name = "Reactor", email = "team@reactor.inc" }
@@ -87,4 +87,4 @@ gst = [
87
87
  # Legacy releases used `X.Y.Z-g<short-sha>` (e.g. `1.4.1-g5660eed`); the wheel
88
88
  # stripped the `-g<sha>` suffix. fetch-proto.py still understands that shape
89
89
  # for back-compat with old pins, but new bumps should track the calendar format.
90
- version = "1.20260514.12095"
90
+ version = "1.20260527.14467"
@@ -31,6 +31,7 @@ class ModelEvent(Enum):
31
31
  CLEANUP_COMPLETE = "cleanup_complete"
32
32
  EVICTION = "eviction"
33
33
  IDLING = "idling"
34
+ SESSION_ACTIVE = "session_active"
34
35
 
35
36
 
36
37
  class ModelStateMachine:
@@ -72,6 +73,15 @@ class ModelStateMachine:
72
73
  ModelEvent.IDLING: [
73
74
  (ModelState.READY, ModelState.READY),
74
75
  ],
76
+ # SESSION_ACTIVE is the analog of IDLING during a live session: a
77
+ # periodic liveness ping sent while the runtime is serving a
78
+ # session (WAITING / STREAMING / ORPHANED). All three transitions
79
+ # are self-loops since the event carries no state change.
80
+ ModelEvent.SESSION_ACTIVE: [
81
+ (ModelState.WAITING, ModelState.WAITING),
82
+ (ModelState.STREAMING, ModelState.STREAMING),
83
+ (ModelState.ORPHANED, ModelState.ORPHANED),
84
+ ],
75
85
  }
76
86
 
77
87
  def __init__(self, initial_state: ModelState = ModelState.CREATED):
@@ -98,7 +108,10 @@ class ModelStateMachine:
98
108
 
99
109
  if to_state is None:
100
110
  log_fn = logger.warning
101
- if event == ModelEvent.IDLING:
111
+ # IDLING and SESSION_ACTIVE are high-frequency liveness pings;
112
+ # legitimate races with state transitions can produce rejected
113
+ # events that don't deserve a warning.
114
+ if event in (ModelEvent.IDLING, ModelEvent.SESSION_ACTIVE):
102
115
  log_fn = logger.debug
103
116
  log_fn(
104
117
  "Illegal state transition",
@@ -108,7 +121,7 @@ class ModelStateMachine:
108
121
  return False
109
122
 
110
123
  log_fn = logger.info
111
- if event == ModelEvent.IDLING:
124
+ if event in (ModelEvent.IDLING, ModelEvent.SESSION_ACTIVE):
112
125
  log_fn = logger.debug
113
126
 
114
127
  log_fn(
@@ -145,7 +158,18 @@ class ModelStateMachine:
145
158
  handler(**kwargs)
146
159
 
147
160
  def _call_enter_handler(self, state: ModelState, previous_state: ModelState):
148
- """Call the appropriate on_enter_<state> method."""
161
+ """Call the appropriate on_enter_<state> method.
162
+
163
+ Self-loop transitions (``state == previous_state``) skip the
164
+ ``on_enter_*`` hook — those hooks are designed to run on actual
165
+ state changes, not on every heartbeat. This matters for
166
+ liveness-ping events like ``IDLING`` and ``SESSION_ACTIVE`` whose
167
+ from/to states are identical: re-running ``on_enter_WAITING`` /
168
+ ``on_enter_ORPHANED`` on every tick would, e.g., cancel and
169
+ restart the orphan timeout, preventing it from ever firing.
170
+ """
171
+ if state == previous_state:
172
+ return
149
173
  handler_name = f"on_enter_{state.value}"
150
174
  handler = getattr(self, handler_name, None)
151
175
  if handler:
@@ -191,6 +215,9 @@ class ModelStateMachine:
191
215
  def on_before_idling(self, **kwargs):
192
216
  pass
193
217
 
218
+ def on_before_session_active(self, **kwargs):
219
+ pass
220
+
194
221
  # on_enter_<state> methods
195
222
  def on_enter_CREATED(self, previous_state: ModelState):
196
223
  pass
@@ -100,6 +100,13 @@ def _iter_strings(value: Any) -> Iterator[str]:
100
100
  _IDLING_INTERVAL_ENV_VAR = "IDLING_INTERVAL_SECONDS"
101
101
  _DEFAULT_IDLING_INTERVAL = 30.0
102
102
 
103
+ # Periodic SESSION_ACTIVE liveness ping. Sent while the runtime is serving
104
+ # a session (WAITING / STREAMING / ORPHANED). Default cadence matches the
105
+ # Supervisor's reaper tick (10s); the Supervisor declares a session lost
106
+ # after 3 missed intervals.
107
+ _SESSION_ACTIVE_INTERVAL_ENV_VAR = "SESSION_ACTIVE_INTERVAL_SECONDS"
108
+ _DEFAULT_SESSION_ACTIVE_INTERVAL = 10.0
109
+
103
110
  _PING_TIMEOUT_ENV_VAR = "WEBRTC_CLIENT_PING_TIMEOUT_SECONDS"
104
111
  _DEFAULT_PING_TIMEOUT = 20.0
105
112
 
@@ -130,6 +137,12 @@ class RuntimeConfig:
130
137
  # Priority: ENV > CLI > default. Set to None to use default.
131
138
  idling_interval: Optional[float] = None
132
139
 
140
+ # Session-active heartbeat interval in seconds. Emitted while the
141
+ # runtime is serving a session (WAITING / STREAMING / ORPHANED) so
142
+ # the Supervisor can detect abruptly terminated sessions.
143
+ # Priority: ENV > CLI > default. Set to None to use default.
144
+ session_active_interval: Optional[float] = None
145
+
133
146
  # WebRTC client-ping watchdog timeout (seconds). Priority: ENV > CLI > default.
134
147
  # Resolved to a concrete float in __post_init__; non-positive disables the watchdog.
135
148
  ping_timeout: Optional[float] = None
@@ -162,6 +175,26 @@ class RuntimeConfig:
162
175
  if self.idling_interval is None:
163
176
  self.idling_interval = _DEFAULT_IDLING_INTERVAL
164
177
 
178
+ # Resolve session_active_interval: ENV > CLI > default, same shape
179
+ # as idling_interval above.
180
+ sa_env_val = os.getenv(_SESSION_ACTIVE_INTERVAL_ENV_VAR)
181
+ if sa_env_val:
182
+ try:
183
+ self.session_active_interval = float(sa_env_val)
184
+ except ValueError:
185
+ logger.warning(
186
+ "Invalid session-active interval env value, falling back to default",
187
+ env_var=_SESSION_ACTIVE_INTERVAL_ENV_VAR,
188
+ value=sa_env_val,
189
+ )
190
+ if (
191
+ self.session_active_interval is not None
192
+ and self.session_active_interval <= 0
193
+ ):
194
+ self.session_active_interval = None
195
+ if self.session_active_interval is None:
196
+ self.session_active_interval = _DEFAULT_SESSION_ACTIVE_INTERVAL
197
+
165
198
  # Resolve ping_timeout: ENV > CLI > default (reject non-finite floats)
166
199
  resolved_ping: Optional[float] = None
167
200
  ping_env = os.getenv(_PING_TIMEOUT_ENV_VAR)
@@ -239,6 +272,17 @@ class RuntimeConfig:
239
272
  help=f"Idling event interval ({_IDLING_INTERVAL_ENV_VAR} env overrides). "
240
273
  f"Default: {_DEFAULT_IDLING_INTERVAL}s",
241
274
  )
275
+ parser.add_argument(
276
+ "--session-active-interval",
277
+ type=float,
278
+ default=None,
279
+ dest="session_active_interval",
280
+ help=(
281
+ "SESSION_ACTIVE liveness event interval, emitted while a "
282
+ f"session is active ({_SESSION_ACTIVE_INTERVAL_ENV_VAR} env "
283
+ f"overrides). Default: {_DEFAULT_SESSION_ACTIVE_INTERVAL}s"
284
+ ),
285
+ )
242
286
  parser.add_argument(
243
287
  "--profiling-dir",
244
288
  type=str,
@@ -291,6 +335,11 @@ class Runtime(ModelStateMachine, ABC):
291
335
  # Idling task - sends periodic IDLING events when in READY state
292
336
  self._idling_task: Optional[asyncio.Task] = None
293
337
 
338
+ # Session-active task - sends periodic SESSION_ACTIVE events while
339
+ # the runtime is serving a session (WAITING / STREAMING / ORPHANED).
340
+ # Lifecycle is gated by on_enter_* state hooks.
341
+ self._session_active_task: Optional[asyncio.Task] = None
342
+
294
343
  # Trickle-ICE buffer
295
344
  # Browsers start emitting ICE candidates as soon as
296
345
  # ``setLocalDescription`` runs — typically *before* the SDP
@@ -478,6 +527,9 @@ class Runtime(ModelStateMachine, ABC):
478
527
  :meth:`add_ice_candidate`.
479
528
  """
480
529
  self._cancel_orphan_timeout()
530
+ # Idempotent: a session arriving here from WAITING will already
531
+ # have armed the loop; re-arming on ORPHANED -> STREAMING is a no-op.
532
+ self._start_session_active_loop()
481
533
 
482
534
  # Replay buffered trickle ICE candidates onto the freshly
483
535
  # installed transport. Scheduled via create_task because
@@ -498,15 +550,24 @@ class Runtime(ModelStateMachine, ABC):
498
550
  """
499
551
  self._clear_pending_ice_candidates()
500
552
  self._start_orphan_timeout()
553
+ # Keep emitting SESSION_ACTIVE while orphaned: the runtime is still
554
+ # "owning" the session, just temporarily without a client.
555
+ self._start_session_active_loop()
501
556
 
502
557
  def on_enter_WAITING(self, previous_state: ModelState) -> None:
503
558
  """Handler called when the session enters the WAITING state.
504
559
  Starts the orphan timeout.
505
560
  """
506
561
  self._start_orphan_timeout()
562
+ # Arm SESSION_ACTIVE pings as soon as the runtime accepts a session,
563
+ # before the client has even connected.
564
+ self._start_session_active_loop()
507
565
 
508
566
  def on_enter_TERMINATED(self, previous_state: ModelState) -> None:
509
567
  """TERMINATED means the runtime should exit."""
568
+ # Cancel the session-active loop synchronously via task.cancel();
569
+ # the awaited stop happens in _shutdown_cleanup.
570
+ self._cancel_session_active_task()
510
571
  self._shutdown_event.set()
511
572
 
512
573
  def on_enter_READY(self, previous_state: ModelState) -> None:
@@ -517,6 +578,8 @@ class Runtime(ModelStateMachine, ABC):
517
578
  shutdown event if SIGTERM was received earlier.
518
579
  """
519
580
  self._clear_pending_ice_candidates()
581
+ # Back to READY: session is fully unwound, stop the per-session loop.
582
+ self._cancel_session_active_task()
520
583
  if self._shutdown_pending:
521
584
  self._shutdown_event.set()
522
585
 
@@ -530,6 +593,9 @@ class Runtime(ModelStateMachine, ABC):
530
593
  """
531
594
  self._cancel_orphan_timeout()
532
595
  self._clear_pending_ice_candidates()
596
+ # Session is wrapping up; stop emitting SESSION_ACTIVE so we don't
597
+ # race the CLEANUP_COMPLETE / STOP_SESSION events.
598
+ self._cancel_session_active_task()
533
599
 
534
600
  if self.model is not None:
535
601
  self.model._push_event(Disconnected())
@@ -1212,6 +1278,7 @@ class Runtime(ModelStateMachine, ABC):
1212
1278
  async def _shutdown_cleanup(self) -> None:
1213
1279
  """Internal shutdown cleanup implementation."""
1214
1280
  await self._stop_idling_loop()
1281
+ await self._stop_session_active_loop()
1215
1282
 
1216
1283
  try:
1217
1284
  if self.session_running or self.current_state == ModelState.WAITING:
@@ -1325,3 +1392,67 @@ class Runtime(ModelStateMachine, ABC):
1325
1392
  break
1326
1393
  except Exception:
1327
1394
  pass
1395
+
1396
+ # -----------------------------------------------------------------
1397
+ # Session-active heartbeat
1398
+ # -----------------------------------------------------------------
1399
+
1400
+ def _start_session_active_loop(self) -> None:
1401
+ """Start the session-active heartbeat loop.
1402
+
1403
+ Emits ``SESSION_ACTIVE`` events while the runtime is serving a
1404
+ session, giving the Supervisor a liveness signal during the
1405
+ WAITING / STREAMING / ORPHANED states. Idempotent: a second call
1406
+ while the loop is already running is a no-op, so it's safe to
1407
+ invoke from every relevant on_enter_* hook.
1408
+ """
1409
+ if self._session_active_task is not None:
1410
+ return
1411
+ self._session_active_task = asyncio.create_task(self._session_active_loop())
1412
+
1413
+ def _cancel_session_active_task(self) -> None:
1414
+ """Cancel the session-active loop synchronously.
1415
+
1416
+ Safe to call from on_enter_* hooks (which run synchronously from
1417
+ ``send()``). The task reference is preserved so that
1418
+ ``_stop_session_active_loop`` (called from ``_shutdown_cleanup``)
1419
+ can still ``await`` the cancelled task during shutdown; the
1420
+ synchronous ``.cancel()`` call is enough for the on_enter_* hooks.
1421
+ """
1422
+ if self._session_active_task is not None:
1423
+ self._session_active_task.cancel()
1424
+
1425
+ async def _stop_session_active_loop(self) -> None:
1426
+ """Cancel the session-active loop and await its exit.
1427
+
1428
+ Called from ``_shutdown_cleanup`` to make sure the loop is fully
1429
+ unwound before the runtime exits.
1430
+ """
1431
+ task = self._session_active_task
1432
+ if task is None:
1433
+ return
1434
+ task.cancel()
1435
+ self._session_active_task = None
1436
+ try:
1437
+ await task
1438
+ except asyncio.CancelledError:
1439
+ pass
1440
+
1441
+ async def _session_active_loop(self) -> None:
1442
+ """Background task that periodically triggers SESSION_ACTIVE events.
1443
+
1444
+ Like ``_idling_loop``, the event is rejected by the state machine
1445
+ unless the runtime is in a busy session state (WAITING / STREAMING
1446
+ / ORPHANED). Rejected events log at debug level, so a late tick
1447
+ racing with a state transition doesn't spam warnings.
1448
+ """
1449
+ assert self.config.session_active_interval is not None
1450
+ interval = self.config.session_active_interval
1451
+ while True:
1452
+ try:
1453
+ await asyncio.sleep(interval)
1454
+ self.send(ModelEvent.SESSION_ACTIVE)
1455
+ except asyncio.CancelledError:
1456
+ break
1457
+ except Exception:
1458
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactor_runtime
3
- Version: 2.7.8
3
+ Version: 2.7.9
4
4
  Summary: Reactor runtime with public model API
5
5
  Author-email: Reactor <team@reactor.inc>
6
6
  Requires-Python: >=3.9