steerable-agent-runtime 0.6.55__tar.gz → 0.6.57__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 (164) hide show
  1. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/PKG-INFO +1 -1
  2. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/pyproject.toml +1 -1
  3. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/calibration.py +75 -16
  4. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/errors.py +5 -1
  5. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/storage/__init__.py +2 -1
  6. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/storage/sqlite_store.py +74 -13
  7. steerable_agent_runtime-0.6.57/src/steerable_agent_runtime/storage/write_lease.py +173 -0
  8. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
  9. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_calibration.py +40 -0
  10. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_write_lease.py +77 -2
  11. steerable_agent_runtime-0.6.55/src/steerable_agent_runtime/storage/write_lease.py +0 -134
  12. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/README.md +0 -0
  13. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/setup.cfg +0 -0
  14. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/__init__.py +0 -0
  15. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/antihallucination.py +0 -0
  16. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/approval.py +0 -0
  17. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/approval_policy.py +0 -0
  18. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/ask_user.py +0 -0
  19. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/branch.py +0 -0
  20. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/cache_control.py +0 -0
  21. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/compaction.py +0 -0
  22. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/compaction_policy.py +0 -0
  23. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/config.py +0 -0
  24. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/default.harness.json +0 -0
  25. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  26. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/delegation_gate.py +0 -0
  27. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  28. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/handoff.py +0 -0
  29. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/harness.py +0 -0
  30. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/harness_spec.py +0 -0
  31. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/history.py +0 -0
  32. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/hooks.py +0 -0
  33. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  34. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  35. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/compat.py +0 -0
  36. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/errors.py +0 -0
  37. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  38. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  39. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  40. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/parts.py +0 -0
  41. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/presets.py +0 -0
  42. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  43. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/loop.py +0 -0
  44. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/maintenance.py +0 -0
  45. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/mcp.py +0 -0
  46. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/mcp_server.py +0 -0
  47. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/model_catalog.py +0 -0
  48. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/model_info.py +0 -0
  49. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/model_resolve.py +0 -0
  50. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/native_bridge.py +0 -0
  51. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/observation_aging.py +0 -0
  52. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/orchestration.py +0 -0
  53. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/otel.py +0 -0
  54. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/plugins.py +0 -0
  55. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/pool.py +0 -0
  56. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/pricing.py +0 -0
  57. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/pseudo.py +0 -0
  58. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/recording.py +0 -0
  59. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/reminders.py +0 -0
  60. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/replay.py +0 -0
  61. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/resume.py +0 -0
  62. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/retry.py +0 -0
  63. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/sandboxed.py +0 -0
  64. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/skills.py +0 -0
  65. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/spill.py +0 -0
  66. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  67. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  68. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/subagent.py +0 -0
  69. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/todo.py +0 -0
  70. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/tokens.py +0 -0
  71. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/tool_presentation.py +0 -0
  72. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/tool_schema.py +0 -0
  73. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/tool_search.py +0 -0
  74. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/tools.py +0 -0
  75. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/tracing.py +0 -0
  76. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  77. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  78. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  79. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime/world_state.py +0 -0
  80. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  81. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  82. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  83. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  84. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_antihallucination.py +0 -0
  85. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_approval.py +0 -0
  86. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_approval_policy.py +0 -0
  87. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_ask_user.py +0 -0
  88. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_branch.py +0 -0
  89. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_cache_control.py +0 -0
  90. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_cache_instrumentation.py +0 -0
  91. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_compaction.py +0 -0
  92. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_compaction_policy.py +0 -0
  93. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_compaction_strategy.py +0 -0
  94. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_config.py +0 -0
  95. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_content_parts.py +0 -0
  96. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_delegation_gate.py +0 -0
  97. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_error_taxonomy.py +0 -0
  98. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_fragment_bounds.py +0 -0
  99. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_gateway_catalog.py +0 -0
  100. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_golden.py +0 -0
  101. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_handoff.py +0 -0
  102. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_harness.py +0 -0
  103. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_harness_spec.py +0 -0
  104. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_history.py +0 -0
  105. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_history_persistence.py +0 -0
  106. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_hooks.py +0 -0
  107. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_in_memory_storage.py +0 -0
  108. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_llm_gemini_wire.py +0 -0
  109. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_llm_responses_wire.py +0 -0
  110. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_llm_stream_e2e.py +0 -0
  111. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_llm_wire_helpers.py +0 -0
  112. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_long_session.py +0 -0
  113. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_loop.py +0 -0
  114. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_loop_cancellation.py +0 -0
  115. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_loop_replay.py +0 -0
  116. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_loop_sandbox_event.py +0 -0
  117. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_loop_terminal.py +0 -0
  118. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_maintenance.py +0 -0
  119. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_mcp.py +0 -0
  120. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_mcp_server.py +0 -0
  121. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_model_catalog.py +0 -0
  122. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_model_equivalence.py +0 -0
  123. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_model_info.py +0 -0
  124. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_model_resolve.py +0 -0
  125. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_native_default.py +0 -0
  126. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_observation_aging.py +0 -0
  127. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_orchestration.py +0 -0
  128. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_otel.py +0 -0
  129. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_parallel_tools.py +0 -0
  130. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_plugins.py +0 -0
  131. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_provider_compat.py +0 -0
  132. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_provider_presets.py +0 -0
  133. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_pseudo.py +0 -0
  134. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_pyo3_api_surface.py +0 -0
  135. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_recording.py +0 -0
  136. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_reminders.py +0 -0
  137. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_replay_crosslang.py +0 -0
  138. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_resume.py +0 -0
  139. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_retry_hooks.py +0 -0
  140. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_safety_gate.py +0 -0
  141. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_sandboxed.py +0 -0
  142. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_skills.py +0 -0
  143. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_soft_timeout.py +0 -0
  144. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_spill.py +0 -0
  145. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_sqlite_storage.py +0 -0
  146. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_steer.py +0 -0
  147. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_storage_contract.py +0 -0
  148. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_stream_strip.py +0 -0
  149. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_subagent.py +0 -0
  150. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_system_proxy.py +0 -0
  151. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_todo.py +0 -0
  152. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_todo_gate.py +0 -0
  153. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_tokens.py +0 -0
  154. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_tool_exposure.py +0 -0
  155. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_tool_hygiene.py +0 -0
  156. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_tool_presentation.py +0 -0
  157. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_tool_router.py +0 -0
  158. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_tool_schema.py +0 -0
  159. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_tool_timeout.py +0 -0
  160. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_trace_recorder.py +0 -0
  161. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_transport_jsonrpc.py +0 -0
  162. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_transport_sse.py +0 -0
  163. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_usage_attribution.py +0 -0
  164. {steerable_agent_runtime-0.6.55 → steerable_agent_runtime-0.6.57}/tests/test_world_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.55
3
+ Version: 0.6.57
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.55"
3
+ version = "0.6.57"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  license = { text = "Apache-2.0" }
6
6
  readme = "README.md"
@@ -25,11 +25,14 @@ from __future__ import annotations
25
25
  import json
26
26
  import os
27
27
  import tempfile
28
+ import time
28
29
  from collections.abc import AsyncIterator, Iterable, Sequence
29
- from dataclasses import asdict, dataclass
30
+ from dataclasses import asdict, dataclass, replace
30
31
  from typing import Any
31
32
 
33
+ from .errors import StoreAlreadyOwnedError
32
34
  from .llm import LLMMessage, LLMProvider, LLMStreamChunk, LLMUsage
35
+ from .storage.write_lease import WriteLease, acquire_write_lease
33
36
  from .tokens import estimate_text_tokens, estimate_tokens, register_model_factor
34
37
 
35
38
  #: Requests needed before a derived factor is trusted and auto-registered.
@@ -70,6 +73,7 @@ class UsageCalibration:
70
73
  self.min_samples = min_samples
71
74
  self.auto_register = auto_register
72
75
  self.models: dict[str, ModelCalibration] = {}
76
+ self._persisted: dict[str, ModelCalibration] = {}
73
77
 
74
78
  def record(
75
79
  self,
@@ -142,29 +146,84 @@ class UsageCalibration:
142
146
  return cal
143
147
 
144
148
  def save(self, path: str) -> None:
145
- """Atomic write: tmp file + rename so a crash mid-write can't corrupt."""
146
- directory = os.path.dirname(os.path.abspath(path))
147
- os.makedirs(directory, exist_ok=True)
148
- fd, tmp = tempfile.mkstemp(dir=directory, suffix=".tmp")
149
+ """Merge this process's new samples into ``path`` and write atomically.
150
+
151
+ The exclusive lease covers the reload and the rename, so two sidecars
152
+ recording different requests both keep their samples. Samples already
153
+ written by this object are not added again.
154
+ """
155
+ lease = _wait_for_file_lease(path)
149
156
  try:
150
- with os.fdopen(fd, "w", encoding="utf-8") as fh:
151
- json.dump(self.to_dict(), fh)
152
- os.replace(tmp, path)
153
- except BaseException:
154
- try:
155
- os.unlink(tmp)
156
- except OSError:
157
- pass
158
- raise
157
+ disk = UsageCalibration.load(
158
+ path, min_samples=self.min_samples, auto_register=False
159
+ )
160
+ self.models = _merge_models(disk.models, self.models, self._persisted)
161
+ self._persisted = _snapshot(self.models)
162
+ _atomic_write(path, self.to_dict())
163
+ finally:
164
+ lease.release()
159
165
 
160
166
  @classmethod
161
167
  def load(cls, path: str, **kwargs: Any) -> UsageCalibration:
162
168
  """Load aggregates from ``path``; missing/corrupt file → empty."""
163
169
  try:
164
170
  with open(path, encoding="utf-8") as fh:
165
- return cls.from_dict(json.load(fh), **kwargs)
171
+ cal = cls.from_dict(json.load(fh), **kwargs)
166
172
  except (OSError, ValueError):
167
- return cls(**kwargs)
173
+ cal = cls(**kwargs)
174
+ cal._persisted = _snapshot(cal.models)
175
+ return cal
176
+
177
+
178
+ def _snapshot(models: dict[str, ModelCalibration]) -> dict[str, ModelCalibration]:
179
+ return {name: replace(entry) for name, entry in models.items()}
180
+
181
+
182
+ def _merge_models(
183
+ disk: dict[str, ModelCalibration],
184
+ local: dict[str, ModelCalibration],
185
+ persisted: dict[str, ModelCalibration],
186
+ ) -> dict[str, ModelCalibration]:
187
+ merged: dict[str, ModelCalibration] = {}
188
+ for name in set(disk) | set(local):
189
+ on_disk = disk.get(name, ModelCalibration())
190
+ current = local.get(name, ModelCalibration())
191
+ base = persisted.get(name, ModelCalibration())
192
+ merged[name] = ModelCalibration(
193
+ requests=on_disk.requests + (current.requests - base.requests),
194
+ est_prompt=on_disk.est_prompt + (current.est_prompt - base.est_prompt),
195
+ obs_prompt=on_disk.obs_prompt + (current.obs_prompt - base.obs_prompt),
196
+ est_completion=on_disk.est_completion + (current.est_completion - base.est_completion),
197
+ obs_completion=on_disk.obs_completion + (current.obs_completion - base.obs_completion),
198
+ )
199
+ return merged
200
+
201
+
202
+ def _wait_for_file_lease(path: str, timeout_s: float = 5.0) -> WriteLease:
203
+ deadline = time.monotonic() + timeout_s
204
+ while True:
205
+ try:
206
+ return acquire_write_lease(path)
207
+ except StoreAlreadyOwnedError:
208
+ if time.monotonic() >= deadline:
209
+ raise
210
+ time.sleep(0.01)
211
+
212
+
213
+ def _atomic_write(path: str, payload: dict[str, Any]) -> None:
214
+ directory = os.path.dirname(os.path.abspath(path))
215
+ os.makedirs(directory, exist_ok=True)
216
+ fd, tmp = tempfile.mkstemp(dir=directory, suffix=".tmp")
217
+ try:
218
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
219
+ json.dump(payload, fh)
220
+ os.replace(tmp, path)
221
+ except BaseException:
222
+ try:
223
+ os.unlink(tmp)
224
+ except OSError:
225
+ pass
226
+ raise
168
227
 
169
228
 
170
229
  @dataclass
@@ -18,8 +18,12 @@ class StorageError(RuntimeError):
18
18
  """Persistence layer failure."""
19
19
 
20
20
 
21
+ class StorageUpgradeBlockedError(StorageError):
22
+ """The database is newer than this process, or a migration cannot take the exclusive lock."""
23
+
24
+
21
25
  class StoreAlreadyOwnedError(StorageError):
22
- """Another process already holds the write lease for this database."""
26
+ """Another process already holds the exclusive lease for this database."""
23
27
 
24
28
  def __init__(self, path: str) -> None:
25
29
  super().__init__(
@@ -117,7 +117,7 @@ class StorageAdapter(Protocol):
117
117
 
118
118
  from .in_memory import InMemoryStorage # noqa: E402
119
119
  from .sqlite_store import SqliteStorage # noqa: E402
120
- from .write_lease import acquire_write_lease, lock_path_for_db # noqa: E402
120
+ from .write_lease import acquire_shared_lease, acquire_write_lease, lock_path_for_db # noqa: E402
121
121
  from ..errors import StoreAlreadyOwnedError # noqa: E402
122
122
 
123
123
  try:
@@ -132,6 +132,7 @@ __all__ = [
132
132
  "SqliteStorage",
133
133
  "SqlAlchemyStorage",
134
134
  "StoreAlreadyOwnedError",
135
+ "acquire_shared_lease",
135
136
  "acquire_write_lease",
136
137
  "lock_path_for_db",
137
138
  ]
@@ -14,13 +14,13 @@ duplicated as real columns with indexes, so session enumeration and the
14
14
  resume tail-scan are indexed lookups, never a table scan over JSON.
15
15
 
16
16
  Concurrency: like ``InMemoryStorage``, all mutations run under one asyncio
17
- lock (safe under concurrent ``await`` in a single loop). A sibling
18
- ``*.lock`` file (``fcntl.flock`` / Windows named mutex) makes the writer
19
- process-exclusive: a second process opening the same path fails loud
20
- (``StoreAlreadyOwnedError``). Process death releases the kernel lock;
21
- there is no TTL steal; the lock file is never deleted. WAL remains so a
22
- reader (e.g. the maintenance CLI) does not block the writer and does not
23
- take the write lease.
17
+ lock (safe under concurrent ``await`` in a single loop). Across processes
18
+ the database uses WAL, ``busy_timeout``, and a shared lease on the sibling
19
+ ``*.lock`` file, so two sidecars can write different sessions. Schema
20
+ migration takes that file exclusively and fails when another process has
21
+ the database open. A ``user_version`` newer than this process refuses to
22
+ open. Process death releases the kernel lock; the lock file is never
23
+ deleted.
24
24
  """
25
25
 
26
26
  from __future__ import annotations
@@ -40,8 +40,11 @@ from steerable_agent_protocol.generated import (
40
40
  TraceSpan,
41
41
  )
42
42
 
43
- from ..errors import StorageError
44
- from .write_lease import WriteLease, acquire_write_lease
43
+ from ..errors import StorageError, StorageUpgradeBlockedError, StoreAlreadyOwnedError
44
+ from .write_lease import WriteLease, acquire_shared_lease, acquire_write_lease
45
+
46
+ SCHEMA_VERSION = 1
47
+ _BUSY_TIMEOUT_MS = 5000
45
48
 
46
49
  _SCHEMA = """
47
50
  PRAGMA journal_mode = WAL;
@@ -107,17 +110,75 @@ def _dump(model: Any) -> str:
107
110
  return json.dumps(model.model_dump(mode="json"), ensure_ascii=False)
108
111
 
109
112
 
113
+ def _is_memory(path: str) -> bool:
114
+ return path == ":memory:" or path.startswith("file::memory:")
115
+
116
+
117
+ def _connect(path: str) -> sqlite3.Connection:
118
+ db = sqlite3.connect(path, timeout=_BUSY_TIMEOUT_MS / 1000, check_same_thread=False)
119
+ db.row_factory = sqlite3.Row
120
+ db.execute(f"PRAGMA busy_timeout = {_BUSY_TIMEOUT_MS}")
121
+ db.execute("PRAGMA journal_mode = WAL")
122
+ db.execute("PRAGMA synchronous = NORMAL")
123
+ return db
124
+
125
+
126
+ def _read_user_version(path: str) -> int:
127
+ db = sqlite3.connect(path, timeout=_BUSY_TIMEOUT_MS / 1000)
128
+ try:
129
+ row = db.execute("PRAGMA user_version").fetchone()
130
+ return int(row[0]) if row else 0
131
+ finally:
132
+ db.close()
133
+
134
+
135
+ def _apply_schema(db: sqlite3.Connection) -> None:
136
+ db.executescript(_SCHEMA)
137
+ db.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
138
+ db.commit()
139
+
140
+
141
+ def _open_lease(path: str) -> WriteLease:
142
+ version = _read_user_version(path)
143
+ if version > SCHEMA_VERSION:
144
+ raise StorageUpgradeBlockedError(
145
+ f"sidecar database version {version} is newer than this program ({SCHEMA_VERSION})"
146
+ )
147
+ if version < SCHEMA_VERSION:
148
+ try:
149
+ exclusive = acquire_write_lease(path)
150
+ except StoreAlreadyOwnedError as exc:
151
+ version = _read_user_version(path)
152
+ if version < SCHEMA_VERSION:
153
+ raise StorageUpgradeBlockedError(
154
+ "sidecar database needs an upgrade but another process has it open"
155
+ ) from exc
156
+ else:
157
+ try:
158
+ db = _connect(path)
159
+ try:
160
+ _apply_schema(db)
161
+ finally:
162
+ db.close()
163
+ finally:
164
+ exclusive.release()
165
+ return acquire_shared_lease(path)
166
+
167
+
110
168
  class SqliteStorage:
111
169
  """Stdlib-sqlite3 StorageAdapter. See module docstring for the contract."""
112
170
 
113
171
  def __init__(self, path: str) -> None:
114
172
  self._path = path
115
173
  self._lock = asyncio.Lock()
116
- self._lease: WriteLease = acquire_write_lease(path)
174
+ self._lease = WriteLease._unheld()
175
+ if _is_memory(path):
176
+ self._db = _connect(path)
177
+ _apply_schema(self._db)
178
+ return
179
+ self._lease = _open_lease(path)
117
180
  try:
118
- self._db = sqlite3.connect(path, check_same_thread=False)
119
- self._db.row_factory = sqlite3.Row
120
- self._db.executescript(_SCHEMA)
181
+ self._db = _connect(path)
121
182
  except BaseException:
122
183
  self._lease.release()
123
184
  raise
@@ -0,0 +1,173 @@
1
+ """Cross-process leases for one sqlite database file.
2
+
3
+ Several processes may hold a shared lease on the sibling ``*.lock`` file
4
+ (``sessions.db`` → ``sessions.lock``) while they have the database open.
5
+ Schema migration takes an exclusive lease and fails immediately when any
6
+ other process holds the file. Process death releases the kernel lock.
7
+ There is no TTL that could steal from a live process. The lock file is
8
+ never deleted: it keeps a stable inode for later lockers.
9
+
10
+ POSIX uses non-blocking ``fcntl.flock`` (``LOCK_SH`` to register an open
11
+ database, ``LOCK_EX`` to migrate). Windows uses the lock file's share mode,
12
+ the same rule as the Rust sidecar: shared openers allow read and write
13
+ sharing, and an exclusive opener allows none. SQLite byte-range locks do
14
+ not see this file, so only this flock/share-mode pair coordinates Python
15
+ and Rust.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import errno
21
+ import os
22
+ import sys
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from ..errors import StoreAlreadyOwnedError
27
+
28
+ _LOCK_ATTEMPTS = 3
29
+
30
+
31
+ def lock_path_for_db(db_path: str | os.PathLike[str]) -> Path:
32
+ """Sibling lock file: ``sessions.db`` → ``sessions.lock``."""
33
+ return Path(db_path).expanduser().resolve().with_suffix(".lock")
34
+
35
+
36
+ def _is_memory(db_path: str) -> bool:
37
+ return db_path == ":memory:" or db_path.startswith("file::memory:")
38
+
39
+
40
+ def acquire_shared_lease(db_path: str | os.PathLike[str]) -> "WriteLease":
41
+ """Register that this process has ``db_path`` open.
42
+
43
+ Other shared holders succeed. An exclusive holder (a migration, or an
44
+ older sidecar that still locks the whole database) fails loud.
45
+ ``:memory:`` databases have no file and take no lease.
46
+ """
47
+ return _acquire(db_path, shared=True)
48
+
49
+
50
+ def acquire_write_lease(db_path: str | os.PathLike[str]) -> "WriteLease":
51
+ """Acquire the exclusive lease for ``db_path``.
52
+
53
+ Used for schema migration. A shared or exclusive holder fails loud.
54
+ ``:memory:`` databases have no file and take no lease.
55
+ """
56
+ return _acquire(db_path, shared=False)
57
+
58
+
59
+ def _acquire(db_path: str | os.PathLike[str], *, shared: bool) -> "WriteLease":
60
+ raw = os.fspath(db_path)
61
+ if _is_memory(raw):
62
+ return WriteLease._unheld()
63
+ lock_path = lock_path_for_db(raw)
64
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
65
+ if sys.platform == "win32":
66
+ return WriteLease._acquire_win32(lock_path, shared=shared)
67
+ return WriteLease._acquire_posix(lock_path, shared=shared)
68
+
69
+
70
+ class WriteLease:
71
+ """Held kernel lock. ``release`` closes the descriptor/handle; the file stays."""
72
+
73
+ def __init__(self, *, _fd: int | None, _handle: Any, _lock_path: Path | None) -> None:
74
+ self._fd = _fd
75
+ self._handle = _handle
76
+ self._lock_path = _lock_path
77
+ self._released = False
78
+
79
+ @classmethod
80
+ def _unheld(cls) -> "WriteLease":
81
+ return cls(_fd=None, _handle=None, _lock_path=None)
82
+
83
+ @classmethod
84
+ def _acquire_posix(cls, lock_path: Path, *, shared: bool) -> "WriteLease":
85
+ import fcntl
86
+
87
+ mode = fcntl.LOCK_SH if shared else fcntl.LOCK_EX
88
+ for _ in range(_LOCK_ATTEMPTS):
89
+ fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o644)
90
+ try:
91
+ fcntl.flock(fd, mode | fcntl.LOCK_NB)
92
+ except OSError as exc:
93
+ os.close(fd)
94
+ if exc.errno in (errno.EAGAIN, errno.EWOULDBLOCK, errno.EACCES):
95
+ raise StoreAlreadyOwnedError(str(lock_path)) from exc
96
+ raise
97
+ held = os.fstat(fd)
98
+ try:
99
+ current = os.stat(lock_path)
100
+ except FileNotFoundError:
101
+ os.close(fd)
102
+ continue
103
+ if (held.st_ino, held.st_dev) == (current.st_ino, current.st_dev):
104
+ return cls(_fd=fd, _handle=None, _lock_path=lock_path)
105
+ os.close(fd)
106
+ raise StoreAlreadyOwnedError(str(lock_path))
107
+
108
+ @classmethod
109
+ def _acquire_win32(cls, lock_path: Path, *, shared: bool) -> "WriteLease":
110
+ import ctypes
111
+ from ctypes import wintypes
112
+
113
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
114
+ create_file = kernel32.CreateFileW
115
+ create_file.argtypes = [
116
+ wintypes.LPCWSTR,
117
+ wintypes.DWORD,
118
+ wintypes.DWORD,
119
+ wintypes.LPVOID,
120
+ wintypes.DWORD,
121
+ wintypes.DWORD,
122
+ wintypes.HANDLE,
123
+ ]
124
+ create_file.restype = wintypes.HANDLE
125
+ generic_read = 0x80000000
126
+ generic_write = 0x40000000
127
+ file_share_read = 0x00000001
128
+ file_share_write = 0x00000002
129
+ open_always = 4
130
+ file_attribute_normal = 0x80
131
+ error_sharing = 32
132
+ error_lock = 33
133
+ invalid = wintypes.HANDLE(-1).value
134
+ share = file_share_read | file_share_write if shared else 0
135
+ handle = create_file(
136
+ str(lock_path),
137
+ generic_read | generic_write,
138
+ share,
139
+ None,
140
+ open_always,
141
+ file_attribute_normal,
142
+ None,
143
+ )
144
+ if handle is None or int(handle) == int(invalid):
145
+ err = ctypes.get_last_error()
146
+ if err in (error_sharing, error_lock):
147
+ raise StoreAlreadyOwnedError(str(lock_path))
148
+ raise ctypes.WinError(err)
149
+ return cls(_fd=None, _handle=handle, _lock_path=lock_path)
150
+
151
+ def release(self) -> None:
152
+ if self._released:
153
+ return
154
+ self._released = True
155
+ if self._fd is not None:
156
+ os.close(self._fd)
157
+ self._fd = None
158
+ if self._handle is not None:
159
+ import ctypes
160
+ from ctypes import wintypes
161
+
162
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
163
+ close_handle = kernel32.CloseHandle
164
+ close_handle.argtypes = [wintypes.HANDLE]
165
+ close_handle.restype = wintypes.BOOL
166
+ close_handle(self._handle)
167
+ self._handle = None
168
+
169
+ def __enter__(self) -> "WriteLease":
170
+ return self
171
+
172
+ def __exit__(self, *exc: object) -> None:
173
+ self.release()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.55
3
+ Version: 0.6.57
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -88,6 +88,46 @@ class TestUsageCalibration:
88
88
  assert loaded.factor("m") == pytest.approx(320 / 400)
89
89
  assert loaded.models["m"].obs_completion == 40
90
90
 
91
+ def test_two_processes_keep_both_models(self, tmp_path) -> None:
92
+ import subprocess
93
+ import sys
94
+
95
+ path = tmp_path / "token-calibration.json"
96
+ script = r"""
97
+ import sys
98
+ import time
99
+ from pathlib import Path
100
+
101
+ from steerable_agent_runtime import UsageCalibration
102
+
103
+ path, model, ready, peer = sys.argv[1:5]
104
+ cal = UsageCalibration.load(path, min_samples=1, auto_register=False)
105
+ Path(ready).write_text("ready", encoding="utf-8")
106
+ deadline = time.monotonic() + 5
107
+ while time.monotonic() < deadline and not Path(peer).exists():
108
+ time.sleep(0.02)
109
+ cal.record(model, est_prompt=100, obs_prompt=80)
110
+ cal.save(path)
111
+ """
112
+ ready_a = tmp_path / "a.ready"
113
+ ready_b = tmp_path / "b.ready"
114
+ proc_a = subprocess.Popen(
115
+ [sys.executable, "-c", script, str(path), "model-a", str(ready_a), str(ready_b)]
116
+ )
117
+ proc_b = subprocess.Popen(
118
+ [sys.executable, "-c", script, str(path), "model-b", str(ready_b), str(ready_a)]
119
+ )
120
+ try:
121
+ assert proc_a.wait(timeout=15) == 0
122
+ assert proc_b.wait(timeout=15) == 0
123
+ finally:
124
+ proc_a.kill()
125
+ proc_b.kill()
126
+ loaded = UsageCalibration.load(str(path), min_samples=1, auto_register=False)
127
+ assert set(loaded.models) == {"model-a", "model-b"}
128
+ assert loaded.models["model-a"].requests == 1
129
+ assert loaded.models["model-b"].requests == 1
130
+
91
131
  def test_load_missing_or_corrupt_returns_empty(self, tmp_path) -> None:
92
132
  assert UsageCalibration.load(str(tmp_path / "nope.json")).models == {}
93
133
  bad = tmp_path / "bad.json"
@@ -7,8 +7,10 @@ import sys
7
7
  import time
8
8
  from pathlib import Path
9
9
 
10
+ import sqlite3
11
+
10
12
  import pytest
11
- from steerable_agent_runtime.errors import StoreAlreadyOwnedError
13
+ from steerable_agent_runtime.errors import StorageUpgradeBlockedError, StoreAlreadyOwnedError
12
14
  from steerable_agent_runtime.storage import SqliteStorage
13
15
  from steerable_agent_runtime.storage.write_lease import (
14
16
  acquire_write_lease,
@@ -62,7 +64,7 @@ def test_second_process_fails_loud(tmp_path: Path) -> None:
62
64
  _wait_held(holder, flag)
63
65
  with pytest.raises(StoreAlreadyOwnedError, match="already owned"):
64
66
  acquire_write_lease(db)
65
- with pytest.raises(StoreAlreadyOwnedError):
67
+ with pytest.raises(StorageUpgradeBlockedError, match="another process"):
66
68
  SqliteStorage(str(db))
67
69
  assert lock_path_for_db(db).is_file()
68
70
  finally:
@@ -87,6 +89,79 @@ def test_successor_opens_after_holder_is_killed(tmp_path: Path) -> None:
87
89
  assert lock_path_for_db(db).is_file()
88
90
 
89
91
 
92
+ _WRITER = r"""
93
+ import asyncio
94
+ import sys
95
+
96
+ from steerable_agent_protocol.generated import AgentSession
97
+ from steerable_agent_runtime.storage import SqliteStorage
98
+
99
+
100
+ async def main() -> None:
101
+ store = SqliteStorage(sys.argv[1])
102
+ await store.upsert_session(
103
+ AgentSession(
104
+ sessionId=sys.argv[2],
105
+ userId="u1",
106
+ chatId=sys.argv[2],
107
+ currentStage="chat",
108
+ isActive=True,
109
+ createdAt="2026-08-30T00:00:00Z",
110
+ updatedAt="2026-08-30T00:00:00Z",
111
+ )
112
+ )
113
+ store.close()
114
+
115
+
116
+ asyncio.run(main())
117
+ """
118
+
119
+
120
+ def test_two_processes_write_different_sessions(tmp_path: Path) -> None:
121
+ """Two sidecars can write different sessions in one database."""
122
+ import asyncio
123
+
124
+ from steerable_agent_protocol.generated import AgentSession
125
+
126
+ db = tmp_path / "sessions.db"
127
+ store = SqliteStorage(str(db))
128
+ try:
129
+ asyncio.run(
130
+ store.upsert_session(
131
+ AgentSession(
132
+ sessionId="s1",
133
+ userId="u1",
134
+ chatId="c1",
135
+ currentStage="chat",
136
+ isActive=True,
137
+ createdAt="2026-08-30T00:00:00Z",
138
+ updatedAt="2026-08-30T00:00:00Z",
139
+ )
140
+ )
141
+ )
142
+ child = subprocess.run(
143
+ [sys.executable, "-c", _WRITER, str(db), "s2"],
144
+ capture_output=True,
145
+ text=True,
146
+ timeout=20,
147
+ )
148
+ assert child.returncode == 0, child.stderr
149
+ loaded = asyncio.run(store.get_session("s2"))
150
+ assert loaded is not None and loaded.sessionId == "s2"
151
+ finally:
152
+ store.close()
153
+
154
+
155
+ def test_refuses_a_database_newer_than_this_program(tmp_path: Path) -> None:
156
+ db = tmp_path / "sessions.db"
157
+ conn = sqlite3.connect(db)
158
+ conn.execute("PRAGMA user_version = 99")
159
+ conn.commit()
160
+ conn.close()
161
+ with pytest.raises(StorageUpgradeBlockedError, match="newer than this program"):
162
+ SqliteStorage(str(db))
163
+
164
+
90
165
  def test_release_does_not_delete_lock_file(tmp_path: Path) -> None:
91
166
  db = tmp_path / "sessions.db"
92
167
  lease = acquire_write_lease(db)