activegraph 1.7.0__tar.gz → 1.7.1__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 (192) hide show
  1. {activegraph-1.7.0/activegraph.egg-info → activegraph-1.7.1}/PKG-INFO +1 -1
  2. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/__init__.py +1 -1
  3. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/sandbox/__init__.py +82 -20
  4. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/sandbox/_child.py +79 -13
  5. {activegraph-1.7.0 → activegraph-1.7.1/activegraph.egg-info}/PKG-INFO +1 -1
  6. {activegraph-1.7.0 → activegraph-1.7.1}/pyproject.toml +1 -1
  7. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_sandbox_trial.py +134 -4
  8. {activegraph-1.7.0 → activegraph-1.7.1}/LICENSE +0 -0
  9. {activegraph-1.7.0 → activegraph-1.7.1}/NOTICE +0 -0
  10. {activegraph-1.7.0 → activegraph-1.7.1}/README.md +0 -0
  11. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/__main__.py +0 -0
  12. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/_signature.py +0 -0
  13. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/behaviors/__init__.py +0 -0
  14. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/behaviors/base.py +0 -0
  15. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/behaviors/decorators.py +0 -0
  16. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/cli/__init__.py +0 -0
  17. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/cli/main.py +0 -0
  18. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/cli/quickstart.py +0 -0
  19. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/cli/renderers.py +0 -0
  20. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/core/__init__.py +0 -0
  21. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/core/clock.py +0 -0
  22. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/core/event.py +0 -0
  23. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/core/graph.py +0 -0
  24. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/core/graph_store.py +0 -0
  25. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/core/ids.py +0 -0
  26. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/core/patch.py +0 -0
  27. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/core/view.py +0 -0
  28. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/errors.py +0 -0
  29. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/frame.py +0 -0
  30. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/__init__.py +0 -0
  31. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/anthropic.py +0 -0
  32. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/cache.py +0 -0
  33. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/embedding.py +0 -0
  34. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/errors.py +0 -0
  35. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/native.py +0 -0
  36. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/openai.py +0 -0
  37. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/parsing.py +0 -0
  38. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/prompt.py +0 -0
  39. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/provider.py +0 -0
  40. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/recorded.py +0 -0
  41. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/types.py +0 -0
  42. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/llm/wire.py +0 -0
  43. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/observability/__init__.py +0 -0
  44. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/observability/logging.py +0 -0
  45. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/observability/metrics.py +0 -0
  46. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/observability/migration.py +0 -0
  47. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/observability/otel.py +0 -0
  48. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/observability/prometheus.py +0 -0
  49. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/observability/status.py +0 -0
  50. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/__init__.py +0 -0
  51. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/__init__.py +0 -0
  52. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/behaviors.py +0 -0
  53. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/fixtures/__init__.py +0 -0
  54. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/fixtures/companies.py +0 -0
  55. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/object_types.py +0 -0
  56. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/prompts/document_researcher.md +0 -0
  57. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/prompts/memo_synthesizer.md +0 -0
  58. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/prompts/question_generator.md +0 -0
  59. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/prompts/risk_identifier.md +0 -0
  60. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/settings.py +0 -0
  61. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/diligence/tools.py +0 -0
  62. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/loader.py +0 -0
  63. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/manifest.py +0 -0
  64. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/packs/scaffold.py +0 -0
  65. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/policy.py +0 -0
  66. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/__init__.py +0 -0
  67. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/_live.py +0 -0
  68. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/behavior_graph.py +0 -0
  69. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/budget.py +0 -0
  70. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/config_errors.py +0 -0
  71. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/diff.py +0 -0
  72. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/errors.py +0 -0
  73. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/exec_errors.py +0 -0
  74. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/patterns.py +0 -0
  75. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/promote.py +0 -0
  76. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/queue.py +0 -0
  77. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/registration_errors.py +0 -0
  78. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/registry.py +0 -0
  79. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/runtime.py +0 -0
  80. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/scheduler.py +0 -0
  81. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/runtime/view_builder.py +0 -0
  82. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/__init__.py +0 -0
  83. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/base.py +0 -0
  84. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/conformance.py +0 -0
  85. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/errors.py +0 -0
  86. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/falkordb.py +0 -0
  87. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/graph_conformance.py +0 -0
  88. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/memory.py +0 -0
  89. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/postgres.py +0 -0
  90. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/retention.py +0 -0
  91. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/serde.py +0 -0
  92. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/sqlite.py +0 -0
  93. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/store/url.py +0 -0
  94. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/tools/__init__.py +0 -0
  95. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/tools/base.py +0 -0
  96. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/tools/cache.py +0 -0
  97. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/tools/context.py +0 -0
  98. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/tools/decorators.py +0 -0
  99. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/tools/errors.py +0 -0
  100. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/tools/graph_query.py +0 -0
  101. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/tools/recorded.py +0 -0
  102. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/tools/web_fetch.py +0 -0
  103. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/trace/__init__.py +0 -0
  104. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/trace/causal.py +0 -0
  105. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph/trace/printer.py +0 -0
  106. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph.egg-info/SOURCES.txt +0 -0
  107. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph.egg-info/dependency_links.txt +0 -0
  108. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph.egg-info/entry_points.txt +0 -0
  109. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph.egg-info/requires.txt +0 -0
  110. {activegraph-1.7.0 → activegraph-1.7.1}/activegraph.egg-info/top_level.txt +0 -0
  111. {activegraph-1.7.0 → activegraph-1.7.1}/setup.cfg +0 -0
  112. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_activate_after.py +0 -0
  113. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_causal_cross_tool.py +0 -0
  114. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_cli.py +0 -0
  115. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_cli_docs_flags.py +0 -0
  116. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_clock.py +0 -0
  117. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_compaction.py +0 -0
  118. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_diff.py +0 -0
  119. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_diligence_pack.py +0 -0
  120. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_diligence_with_tools.py +0 -0
  121. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_disable_pack.py +0 -0
  122. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_doc_links.py +0 -0
  123. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_doc_python_snippets.py +0 -0
  124. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_doc_site_reachable.py +0 -0
  125. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_embedding_provider.py +0 -0
  126. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_errors_format.py +0 -0
  127. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_event.py +0 -0
  128. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_falkordb_store.py +0 -0
  129. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_fork.py +0 -0
  130. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_graph.py +0 -0
  131. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_graph_store.py +0 -0
  132. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_ids.py +0 -0
  133. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_license.py +0 -0
  134. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_anthropic.py +0 -0
  135. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_behavior.py +0 -0
  136. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_budget.py +0 -0
  137. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_causal.py +0 -0
  138. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_claim_extraction.py +0 -0
  139. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_default_model.py +0 -0
  140. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_determinism.py +0 -0
  141. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_failure.py +0 -0
  142. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_native_structured_output.py +0 -0
  143. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_openai.py +0 -0
  144. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_prompt.py +0 -0
  145. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_provider_fixtures.py +0 -0
  146. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_replay.py +0 -0
  147. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_tool_loop.py +0 -0
  148. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_trace.py +0 -0
  149. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_trace_snapshot.py +0 -0
  150. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_types.py +0 -0
  151. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llm_wire.py +0 -0
  152. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_llms_txt.py +0 -0
  153. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_manifest_warning_tier.py +0 -0
  154. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_migration.py +0 -0
  155. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_observability_logging.py +0 -0
  156. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_observability_metrics.py +0 -0
  157. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_operate_example.py +0 -0
  158. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_pack_manifest.py +0 -0
  159. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_pack_scaffold.py +0 -0
  160. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_packs.py +0 -0
  161. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_patch.py +0 -0
  162. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_pattern_matcher.py +0 -0
  163. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_pattern_parser.py +0 -0
  164. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_pattern_subscriptions.py +0 -0
  165. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_persistence.py +0 -0
  166. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_postgres_store.py +0 -0
  167. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_promote.py +0 -0
  168. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_quickstart.py +0 -0
  169. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_quickstart_snapshot.py +0 -0
  170. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_reason_codes_docs.py +0 -0
  171. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_registration_validation.py +0 -0
  172. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_replay.py +0 -0
  173. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_replay_trace_snapshot.py +0 -0
  174. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_requeue_unfired.py +0 -0
  175. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_resume_example.py +0 -0
  176. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_runtime.py +0 -0
  177. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_runtime_status.py +0 -0
  178. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_serde.py +0 -0
  179. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_store_conformance.py +0 -0
  180. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_store_url.py +0 -0
  181. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_tool_replay.py +0 -0
  182. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_tool_trace_snapshot.py +0 -0
  183. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_tools.py +0 -0
  184. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_trace.py +0 -0
  185. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_trace_accessors.py +0 -0
  186. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_tutorial_snippets.py +0 -0
  187. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_v1_0_1_patches.py +0 -0
  188. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_v1_0_3_behavior_failed_ux.py +0 -0
  189. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_v1_0_3_tool_multiturn.py +0 -0
  190. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_version_sync.py +0 -0
  191. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_view.py +0 -0
  192. {activegraph-1.7.0 → activegraph-1.7.1}/tests/test_wheel_completeness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.7.0
3
+ Version: 1.7.1
4
4
  Summary: An event-sourced reactive graph runtime for long-running, auditable, agentic systems.
5
5
  Author: Active Graph contributors
6
6
  License-Expression: Apache-2.0
@@ -242,4 +242,4 @@ __all__ = [
242
242
  "tool",
243
243
  ]
244
244
 
245
- __version__ = "1.7.0"
245
+ __version__ = "1.7.1"
@@ -59,6 +59,7 @@ pack's static gates remain the pre-execution filter.
59
59
  from __future__ import annotations
60
60
 
61
61
  import json
62
+ import logging
62
63
  import os
63
64
  import subprocess
64
65
  import sys
@@ -66,6 +67,8 @@ from dataclasses import dataclass, field
66
67
  from pathlib import Path
67
68
  from typing import Optional
68
69
 
70
+ _log = logging.getLogger("activegraph.sandbox")
71
+
69
72
 
70
73
  #: The closed outcome set. ``crashed`` is the implementation's one
71
74
  #: addition over the design draft: a child that dies without a
@@ -144,6 +147,12 @@ class TrialReport:
144
147
  favor of the store. Richer evidence (tracebacks, diffs) is read
145
148
  from the fork's log directly: ``Runtime.load(store, run_id=
146
149
  report.fork_run_id)`` then ``trace.failures()`` / ``diff()``.
150
+
151
+ ``warnings`` (v1.7.1) carries any resource net that DEGRADED on
152
+ this platform — most notably the memory cap (RLIMIT_AS) on macOS,
153
+ which the Darwin kernel refuses. A degraded net is announced, never
154
+ silently skipped: the same strings are also folded into ``detail``,
155
+ and the parent logs each one.
147
156
  """
148
157
 
149
158
  outcome: str
@@ -152,6 +161,7 @@ class TrialReport:
152
161
  behavior_failures: int
153
162
  detail: str
154
163
  exit_code: Optional[int]
164
+ warnings: tuple[str, ...] = ()
155
165
 
156
166
 
157
167
  class SandboxStartupError(RuntimeError):
@@ -277,22 +287,52 @@ def _run_child(
277
287
  return proc.returncode, stdout, stderr, True
278
288
 
279
289
 
290
+ #: A representative memory cap the preflight probe applies when the
291
+ #: caller left ``max_rss_bytes`` unset — generous enough not to cap a
292
+ #: just-started child into a false ``MemoryError`` on Linux (a child
293
+ #: after importing activegraph sits near ~40 MiB of address space),
294
+ #: finite enough to actually exercise the ``setrlimit(RLIMIT_AS)`` call
295
+ #: that Darwin rejects. The point of the probe is to trip the platform
296
+ #: check, not to bound anything.
297
+ _PREFLIGHT_PROBE_RSS = 1024 * 2**20
298
+
299
+
300
+ def _limits_job_block(limits: TrialLimits, *, probe: bool) -> dict[str, object]:
301
+ """The ``limits`` sub-dict of a child job. ``probe=True`` fills an
302
+ unset ``max_rss_bytes`` with :data:`_PREFLIGHT_PROBE_RSS` so
303
+ preflight always exercises the RLIMIT_AS path."""
304
+ max_rss = limits.max_rss_bytes
305
+ if probe and max_rss is None:
306
+ max_rss = _PREFLIGHT_PROBE_RSS
307
+ return {
308
+ "max_rss_bytes": max_rss,
309
+ "max_events": limits.max_events,
310
+ "max_llm_calls": limits.max_llm_calls,
311
+ "cpu_seconds": int(limits.wall_clock_seconds) + 5,
312
+ }
313
+
314
+
280
315
  def _preflight_with(
281
316
  env: dict[str, str],
282
317
  *,
318
+ limits: TrialLimits = TrialLimits(),
283
319
  python_flags: tuple[str, ...] = (),
284
320
  timeout: float = 30.0,
285
- ) -> None:
321
+ ) -> tuple[str, ...]:
286
322
  """The preflight check against a specific ``env`` (the testable
287
- seam under :func:`preflight`). Raises :class:`SandboxStartupError`
288
- if a child cannot start and report ``preflight: ok``.
323
+ seam under :func:`preflight`). Applies a representative limits block
324
+ so the resource-limit path is actually exercised. Returns any
325
+ degradation warnings (empty when every requested net applied);
326
+ raises :class:`SandboxStartupError` if a child cannot start and
327
+ report ``preflight: ok``.
289
328
  """
329
+ job = {"preflight": True, "limits": _limits_job_block(limits, probe=True)}
290
330
  exit_code, stdout, stderr, timed_out = _run_child(
291
- {"preflight": True}, env=env, wall_clock=timeout, python_flags=python_flags
331
+ job, env=env, wall_clock=timeout, python_flags=python_flags
292
332
  )
293
333
  tail = _parse_report_tail(stdout)
294
334
  if not timed_out and exit_code == 0 and tail.get("preflight") == "ok":
295
- return
335
+ return tuple(str(w) for w in (tail.get("warnings") or ()))
296
336
  cause = _stderr_tail(stderr) or (
297
337
  "child timed out" if timed_out else "no preflight tail on stdout"
298
338
  )
@@ -301,18 +341,33 @@ def _preflight_with(
301
341
  )
302
342
 
303
343
 
304
- def preflight(*, limits: TrialLimits = TrialLimits(), timeout: float = 30.0) -> None:
344
+ def preflight(
345
+ *, limits: TrialLimits = TrialLimits(), timeout: float = 30.0
346
+ ) -> tuple[str, ...]:
305
347
  """Verify a trial child can START under the sandbox env on this box.
306
348
 
307
- Spawns the child with a null job (no fork, no store, no candidate):
308
- it imports ``activegraph``, echoes ``preflight: ok``, and exits 0.
309
- Returns ``None`` on success; raises :class:`SandboxStartupError`
310
- with the child's stderr tail otherwise. Consumers call this once at
311
- boot so a broken sandbox env (e.g. a platform whose package
312
- discovery the allow-list strips) fails LOUD here instead of showing
313
- up as an opaque ``crashed`` on the first real trial.
349
+ Spawns the child with a null job that exercises the full startup
350
+ path a real trial hits — importing ``activegraph`` under the sandbox
351
+ env AND applying resource limits (v1.7.1: a go/no-go gate that
352
+ skipped limit application passed on macOS where every real trial
353
+ then crashed on RLIMIT_AS). No fork, no store, no candidate.
354
+
355
+ Returns a tuple of degradation warnings empty when every net
356
+ applied cleanly, non-empty (and logged) when a net degraded on this
357
+ platform (e.g. the memory cap on macOS). Raises
358
+ :class:`SandboxStartupError` with the child's stderr tail if a child
359
+ cannot start at all. So on macOS this PASSES with a memory-net
360
+ warning rather than pass-then-crash; on a box that cannot import
361
+ activegraph it RAISES with the real cause. Consumers call it once at
362
+ boot to fail loud or learn what degraded before the first trial.
363
+
364
+ Pass the same ``limits`` a real trial will carry to probe that exact
365
+ configuration; the default probes with a representative memory cap.
314
366
  """
315
- _preflight_with(_child_env(limits), timeout=timeout)
367
+ warnings = _preflight_with(_child_env(limits), limits=limits, timeout=timeout)
368
+ for w in warnings:
369
+ _log.warning("sandbox preflight: %s", w)
370
+ return warnings
316
371
 
317
372
 
318
373
  def run_forked_trial(
@@ -379,12 +434,7 @@ def run_forked_trial(
379
434
  for p in extra_packs
380
435
  ],
381
436
  "scenario": scenario,
382
- "limits": {
383
- "max_rss_bytes": limits.max_rss_bytes,
384
- "max_events": limits.max_events,
385
- "max_llm_calls": limits.max_llm_calls,
386
- "cpu_seconds": int(limits.wall_clock_seconds) + 5,
387
- },
437
+ "limits": _limits_job_block(limits, probe=False),
388
438
  }
389
439
  exit_code, stdout, stderr, timed_out = _run_child(
390
440
  job, env=_child_env(limits), wall_clock=limits.wall_clock_seconds
@@ -418,6 +468,17 @@ def run_forked_trial(
418
468
  if err_tail:
419
469
  detail = f"{detail}; child stderr:\n{err_tail}"
420
470
 
471
+ # Degraded resource nets (v1.7.1): announced, never silent. A memory
472
+ # cap the platform refused (RLIMIT_AS on macOS) is folded into detail
473
+ # and logged, so a trial that ran WITHOUT the memory net it asked for
474
+ # is loud rather than quietly unbounded.
475
+ warnings = tuple(str(w) for w in (tail.get("warnings") or ()))
476
+ if warnings:
477
+ joined = "; ".join(warnings)
478
+ detail = f"{detail} [degraded: {joined}]" if detail else f"[degraded: {joined}]"
479
+ for w in warnings:
480
+ _log.warning("sandbox trial %s: %s", fork_run_id, w)
481
+
421
482
  # The store is the record: re-read the fork run for the numbers.
422
483
  events_appended = 0
423
484
  behavior_failures = 0
@@ -437,4 +498,5 @@ def run_forked_trial(
437
498
  behavior_failures=behavior_failures,
438
499
  detail=detail,
439
500
  exit_code=exit_code,
501
+ warnings=warnings,
440
502
  )
@@ -22,6 +22,9 @@ from pathlib import Path
22
22
  from typing import Any, Callable, Optional
23
23
 
24
24
 
25
+ _LIMIT_WARNINGS: list[str] = []
26
+
27
+
25
28
  def _report(
26
29
  outcome: str,
27
30
  *,
@@ -39,6 +42,11 @@ def _report(
39
42
  "events_appended": events_appended,
40
43
  "behavior_failures": behavior_failures,
41
44
  "detail": detail[:500],
45
+ # Degraded-limit warnings (v1.7.1): a resource net that
46
+ # could not be applied on this platform is announced,
47
+ # never silently skipped. The parent folds these into
48
+ # TrialReport.warnings and TrialReport.detail.
49
+ "warnings": list(_LIMIT_WARNINGS),
42
50
  }
43
51
  ),
44
52
  flush=True,
@@ -46,19 +54,67 @@ def _report(
46
54
  raise SystemExit(exit_code)
47
55
 
48
56
 
49
- def _apply_rlimits(limits: dict[str, Any]) -> None:
50
- """POSIX resource caps; on platforms without ``resource`` the
51
- wall-clock and budget nets still hold (stated in the design)."""
57
+ def _apply_rlimits(limits: dict[str, Any]) -> list[str]:
58
+ """Apply POSIX resource caps portably, returning a warning for any
59
+ cap that could not be set on this platform.
60
+
61
+ RLIMIT_AS (the memory net) is settable on Linux but rejected by the
62
+ Darwin kernel (``ValueError: current limit exceeds maximum limit``
63
+ — Darwin does not support address-space limiting). Rather than
64
+ crash (a trial child that can't start is useless) or silently skip
65
+ (a memory net that quietly does nothing is worse than one that
66
+ announces it is off), an unsettable cap DEGRADES: we record a loud
67
+ warning and let the wall-clock kill and event budget — which are
68
+ unaffected — remain the active nets. We never RAISE a hard limit
69
+ (that needs privilege and is what triggered the Darwin crash): the
70
+ target is clamped to the existing hard limit, so the call only ever
71
+ lowers, and the strong soft==hard cap is preserved where the kernel
72
+ allows it. Returns the list of degradation warnings (empty when all
73
+ requested caps applied).
74
+ """
75
+ warnings: list[str] = []
52
76
  try:
53
77
  import resource
54
78
  except ImportError: # pragma: no cover — Windows
55
- return
79
+ if limits.get("max_rss_bytes"):
80
+ warnings.append(_mem_off("no `resource` module on this platform"))
81
+ return warnings
82
+
83
+ def _cap(kind: str, name: str, requested: int) -> None:
84
+ limit = getattr(resource, name)
85
+ _, hard = resource.getrlimit(limit)
86
+ target = (
87
+ requested
88
+ if hard == resource.RLIM_INFINITY
89
+ else min(requested, hard)
90
+ )
91
+ try:
92
+ resource.setrlimit(limit, (target, target))
93
+ except (ValueError, OSError) as e:
94
+ warnings.append(
95
+ _mem_off(f"{sys.platform}: {type(e).__name__}: {e}")
96
+ if kind == "memory"
97
+ else (
98
+ f"CPU cap (RLIMIT_CPU) could not be applied on "
99
+ f"{sys.platform}: {type(e).__name__}: {e}"
100
+ )
101
+ )
102
+
56
103
  max_rss = limits.get("max_rss_bytes")
57
104
  if max_rss:
58
- resource.setrlimit(resource.RLIMIT_AS, (max_rss, max_rss))
105
+ _cap("memory", "RLIMIT_AS", max_rss)
59
106
  cpu = limits.get("cpu_seconds")
60
107
  if cpu:
61
- resource.setrlimit(resource.RLIMIT_CPU, (cpu, cpu))
108
+ _cap("cpu", "RLIMIT_CPU", cpu)
109
+ return warnings
110
+
111
+
112
+ def _mem_off(reason: str) -> str:
113
+ return (
114
+ f"memory cap (RLIMIT_AS) could not be applied ({reason}); the "
115
+ f"memory net is OFF for this trial — the wall-clock kill and "
116
+ f"event budget still bound a runaway"
117
+ )
62
118
 
63
119
 
64
120
  def _materialize_pack(job: dict[str, Any]) -> Any:
@@ -138,18 +194,28 @@ def _resolve_scenario(
138
194
  def main() -> None:
139
195
  job = json.loads(sys.stdin.read())
140
196
 
141
- # Preflight (v1.7): a null job that only proves the child could
142
- # START i.e. that importing activegraph under the sandbox env
143
- # succeeded, which happens before this function even runs. Reaching
144
- # here at all means the heavy import chain worked; echo the marker
145
- # and exit clean. No fork, no store, no candidate touched.
197
+ # Apply resource limits BEFORE the preflight branch (v1.7.1): the
198
+ # limit-application path is exactly what varies by host (Darwin
199
+ # rejects RLIMIT_AS), so preflight must exercise it to be a real
200
+ # go/no-go gate a null job that skipped it would pass on a box
201
+ # where every real trial then crashed. Degradation warnings are
202
+ # stashed for _report and echoed on the preflight tail.
203
+ _LIMIT_WARNINGS.extend(_apply_rlimits(job.get("limits", {})))
204
+
205
+ # Preflight (v1.7): a null job that proves the child could START —
206
+ # importing activegraph under the sandbox env succeeded (before this
207
+ # function ran) AND resource limits could be applied (just above).
208
+ # Echo the marker plus any degradation warnings and exit clean. No
209
+ # fork, no store, no candidate touched.
146
210
  if job.get("preflight"):
147
- print(json.dumps({"preflight": "ok"}), flush=True)
211
+ print(
212
+ json.dumps({"preflight": "ok", "warnings": list(_LIMIT_WARNINGS)}),
213
+ flush=True,
214
+ )
148
215
  raise SystemExit(0)
149
216
 
150
217
  fork_run_id = str(job["fork_run_id"])
151
218
  initial = int(job.get("initial_events", 0))
152
- _apply_rlimits(job.get("limits", {}))
153
219
 
154
220
  def counts(rt: Any) -> tuple[int, int]:
155
221
  return (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: activegraph
3
- Version: 1.7.0
3
+ Version: 1.7.1
4
4
  Summary: An event-sourced reactive graph runtime for long-running, auditable, agentic systems.
5
5
  Author: Active Graph contributors
6
6
  License-Expression: Apache-2.0
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "activegraph"
10
- version = "1.7.0"
10
+ version = "1.7.1"
11
11
  description = "An event-sourced reactive graph runtime for long-running, auditable, agentic systems."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -540,9 +540,9 @@ def test_child_import_crash_surfaces_the_cause_in_detail(tmp_path, monkeypatch):
540
540
 
541
541
 
542
542
  def test_preflight_succeeds_on_a_working_env():
543
- # Returns None (no raise) when a child can start under the sandbox
544
- # env — the normal case on any box where the parent imports fine.
545
- assert preflight(timeout=30.0) is None
543
+ # Returns () (no raise, no degradation) when a child can start AND
544
+ # apply limits under the sandbox env — the normal case on Linux.
545
+ assert preflight(timeout=30.0) == ()
546
546
 
547
547
 
548
548
  def test_preflight_fails_loud_with_the_cause_on_a_restricted_env():
@@ -570,7 +570,7 @@ def test_explicit_code_channel_rescues_a_restricted_child():
570
570
  python_flags=("-S",),
571
571
  timeout=30.0,
572
572
  )
573
- is None
573
+ == ()
574
574
  )
575
575
 
576
576
 
@@ -598,3 +598,133 @@ def test_env_passthrough_still_forwards_named_vars(monkeypatch):
598
598
  env = sandbox._child_env(TrialLimits(env_passthrough=("MY_TRIAL_FLAG",)))
599
599
  assert env["MY_TRIAL_FLAG"] == "1"
600
600
  assert "PYTHONPATH" in env
601
+
602
+
603
+ # ------------------- portable RLIMIT_AS (v1.7.1 macOS soak fix)
604
+
605
+ resource = pytest.importorskip("resource")
606
+ from activegraph.sandbox import _child # noqa: E402
607
+
608
+
609
+ def test_rlimit_as_applies_cleanly_on_linux():
610
+ # Linux shape: a settable RLIMIT_AS applies with no degradation.
611
+ warnings = _child._apply_rlimits(
612
+ {"max_rss_bytes": 1024 * 2**20, "cpu_seconds": 60}
613
+ )
614
+ assert warnings == []
615
+
616
+
617
+ def test_rlimit_as_rejection_degrades_not_crashes(monkeypatch):
618
+ # macOS shape (simulated): Darwin rejects setrlimit(RLIMIT_AS) with
619
+ # exactly this ValueError. The child must NOT crash and must NOT
620
+ # silently skip — it degrades with a loud, announced warning. This
621
+ # is the regression guard for the macOS rotation-1 crash.
622
+ real = resource.setrlimit
623
+
624
+ def darwin(which, pair):
625
+ if which == resource.RLIMIT_AS:
626
+ raise ValueError("current limit exceeds maximum limit")
627
+ return real(which, pair)
628
+
629
+ monkeypatch.setattr(resource, "setrlimit", darwin)
630
+ warnings = _child._apply_rlimits(
631
+ {"max_rss_bytes": 256 * 2**20, "cpu_seconds": 60}
632
+ )
633
+ assert len(warnings) == 1
634
+ assert "memory net is OFF" in warnings[0]
635
+ assert "RLIMIT_AS" in warnings[0]
636
+ # The CPU cap (which Darwin DOES accept) still applied — only the
637
+ # unsupported net degraded.
638
+ assert "RLIMIT_CPU" not in warnings[0]
639
+
640
+
641
+ def test_rlimit_never_raises_the_hard_limit(monkeypatch):
642
+ # The Darwin crash was setrlimit raising the hard limit; we clamp to
643
+ # the existing hard limit so the call only ever LOWERS. Prove the
644
+ # target passed to setrlimit never exceeds the current hard limit.
645
+ seen = {}
646
+ real_get = resource.getrlimit
647
+ monkeypatch.setattr(
648
+ resource, "getrlimit", lambda w: (0, 512 * 2**20) # finite hard
649
+ )
650
+
651
+ def capture(which, pair):
652
+ seen[which] = pair
653
+
654
+ monkeypatch.setattr(resource, "setrlimit", capture)
655
+ _child._apply_rlimits({"max_rss_bytes": 4096 * 2**20}) # ask for MORE
656
+ soft, hard = seen[resource.RLIMIT_AS]
657
+ assert soft == hard == 512 * 2**20 # clamped to existing hard, not raised
658
+
659
+
660
+ def test_preflight_exercises_the_limit_path():
661
+ # Fix 2: preflight must send a limits block so it reaches
662
+ # _apply_rlimits — a null job that skipped it passed on macOS where
663
+ # every real trial then crashed.
664
+ probe = sandbox._limits_job_block(TrialLimits(), probe=True)
665
+ assert probe["max_rss_bytes"] == sandbox._PREFLIGHT_PROBE_RSS
666
+ # A caller's explicit cap is probed as-is.
667
+ probe2 = sandbox._limits_job_block(
668
+ TrialLimits(max_rss_bytes=333 * 2**20), probe=True
669
+ )
670
+ assert probe2["max_rss_bytes"] == 333 * 2**20
671
+ # A real trial's block keeps the caller's None (no probe fill).
672
+ real = sandbox._limits_job_block(TrialLimits(), probe=False)
673
+ assert real["max_rss_bytes"] is None
674
+
675
+
676
+ def test_trial_report_surfaces_degraded_warning(tmp_path, monkeypatch):
677
+ # End-to-end parent surfacing: a child that reports a degraded net
678
+ # must land it in TrialReport.warnings AND detail (loud), on an
679
+ # otherwise-completed trial.
680
+ path, parent_run, tip, n_parent = _parent_store(tmp_path)
681
+ root, bundle = _candidate_dir(tmp_path)
682
+
683
+ degraded = "memory cap (RLIMIT_AS) could not be applied (darwin: ...); the memory net is OFF for this trial"
684
+
685
+ def fake_run_child(job, *, env, wall_clock, python_flags=()):
686
+ tail = {
687
+ "outcome": "completed",
688
+ "fork_run_id": job["fork_run_id"],
689
+ "events_appended": 1,
690
+ "behavior_failures": 0,
691
+ "detail": "",
692
+ "warnings": [degraded],
693
+ }
694
+ import json as _json
695
+
696
+ return 0, _json.dumps(tail), "", False
697
+
698
+ monkeypatch.setattr(sandbox, "_run_child", fake_run_child)
699
+ report = run_forked_trial(
700
+ path,
701
+ parent_run_id=parent_run,
702
+ at_event=tip,
703
+ pack_source=PackSource(root_dir=str(root), expected_bundle_hash=bundle),
704
+ scenario="scenario.py",
705
+ limits=TrialLimits(max_rss_bytes=256 * 2**20),
706
+ )
707
+ assert report.outcome == "completed"
708
+ assert report.warnings == (degraded,)
709
+ assert "degraded" in report.detail
710
+ assert "memory net is OFF" in report.detail
711
+
712
+
713
+ def test_real_trial_with_memory_cap_completes_on_linux(tmp_path):
714
+ # Integration: a real trial carrying a memory cap runs clean on
715
+ # Linux (the net applied, no degradation) — proving the portable
716
+ # path did not break the platform where RLIMIT_AS works.
717
+ path, parent_run, tip, n_parent = _parent_store(tmp_path)
718
+ root, bundle = _candidate_dir(tmp_path)
719
+
720
+ report = run_forked_trial(
721
+ path,
722
+ parent_run_id=parent_run,
723
+ at_event=tip,
724
+ pack_source=PackSource(root_dir=str(root), expected_bundle_hash=bundle),
725
+ scenario="scenario.py",
726
+ limits=TrialLimits(max_rss_bytes=1024 * 2**20, wall_clock_seconds=60),
727
+ )
728
+ assert report.outcome == "completed", report.detail
729
+ assert report.warnings == ()
730
+ _parent_untouched(path, parent_run, n_parent)
File without changes
File without changes
File without changes