aether-robotics 5.2.2__tar.gz → 5.2.4__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 (266) hide show
  1. {aether_robotics-5.2.2/aether_robotics.egg-info → aether_robotics-5.2.4}/PKG-INFO +17 -1
  2. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/README.md +16 -0
  3. aether_robotics-5.2.4/VERSION +1 -0
  4. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/__init__.py +8 -1
  5. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/app.py +104 -18
  6. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/auto_updater.py +30 -2
  7. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/boot_timer.py +59 -1
  8. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/embeddings.py +102 -5
  9. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/memory.py +16 -0
  10. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/navigation_engine.py +31 -9
  11. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/plan_cache.py +72 -0
  12. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/tool_builder.py +64 -17
  13. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/tool_discovery.py +7 -0
  14. {aether_robotics-5.2.2 → aether_robotics-5.2.4/aether_robotics.egg-info}/PKG-INFO +17 -1
  15. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/pyproject.toml +1 -1
  16. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_plan_cache.py +300 -1
  17. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_retrieval_planner_contract.py +1 -1
  18. aether_robotics-5.2.2/VERSION +0 -1
  19. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/LICENSE +0 -0
  20. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/MANIFEST.in +0 -0
  21. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/__main__.py +0 -0
  22. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/actions/__init__.py +0 -0
  23. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/actions/abstract_actions.py +0 -0
  24. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/__init__.py +0 -0
  25. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/_generator.py +0 -0
  26. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/adeept_hat_v3.py +0 -0
  27. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/base_adapter.py +0 -0
  28. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/drone_adapter.py +0 -0
  29. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/freenove_4wd.py +0 -0
  30. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/freenove_mecanum.py +0 -0
  31. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/gopigo3.py +0 -0
  32. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/l298n_direct_gpio.py +0 -0
  33. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/pca9685_generic.py +0 -0
  34. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/rover_adapter.py +0 -0
  35. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/sunfounder_picarx.py +0 -0
  36. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/sunfounder_pidog.py +0 -0
  37. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/universal_adapter.py +0 -0
  38. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/waveshare_motor_driver.py +0 -0
  39. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/adapters/yahboom_robot_hat.py +0 -0
  40. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/__init__.py +0 -0
  41. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/adaptation_agent.py +0 -0
  42. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/camera_agent.py +0 -0
  43. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/correction_agent.py +0 -0
  44. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/execution_agent.py +0 -0
  45. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/fault_agent.py +0 -0
  46. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/memory_agent.py +0 -0
  47. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/movement_agent.py +0 -0
  48. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/navigation_agent.py +0 -0
  49. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/perception_agent.py +0 -0
  50. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/planner_agent.py +0 -0
  51. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/power_agent.py +0 -0
  52. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/task_manager.py +0 -0
  53. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/agents/thermal_agent.py +0 -0
  54. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/capabilities/__init__.py +0 -0
  55. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/capabilities/capability_loader.py +0 -0
  56. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/__init__.py +0 -0
  57. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/analytics.py +0 -0
  58. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/autonomous.py +0 -0
  59. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/calibrate.py +0 -0
  60. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/doctor.py +0 -0
  61. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/fix.py +0 -0
  62. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits.py +0 -0
  63. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/freenove_4wd.yaml +0 -0
  64. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/gopigo3.yaml +0 -0
  65. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/sunfounder_picarx.yaml +0 -0
  66. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/sunfounder_pidog.yaml +0 -0
  67. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/kits_install_recipes/yahboom_robot_hat.yaml +0 -0
  68. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/cli/memory.py +0 -0
  69. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/__init__.py +0 -0
  70. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/analytics.py +0 -0
  71. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/auto_installer.py +0 -0
  72. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/autonomous.py +0 -0
  73. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/banner.py +0 -0
  74. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/calibration.py +0 -0
  75. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/decomposer.py +0 -0
  76. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/discovery_cache.py +0 -0
  77. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/executor.py +0 -0
  78. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/feedback.py +0 -0
  79. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/feedback_prompt.py +0 -0
  80. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/firstrun.py +0 -0
  81. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/genome.py +0 -0
  82. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/goal_parser.py +0 -0
  83. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/goals.py +0 -0
  84. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/intent.py +0 -0
  85. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/json_repair.py +0 -0
  86. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/llm_planner.py +0 -0
  87. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/mapper.py +0 -0
  88. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/mavlink_integration.py +0 -0
  89. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/message_bus.py +0 -0
  90. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/metrics.py +0 -0
  91. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/net_guard.py +0 -0
  92. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/overrides.py +0 -0
  93. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/parallel_safety.py +0 -0
  94. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/patches.py +0 -0
  95. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/permissions.py +0 -0
  96. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/planner.py +0 -0
  97. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/__init__.py +0 -0
  98. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/_cache.py +0 -0
  99. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/aether_cloud.py +0 -0
  100. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/anthropic.py +0 -0
  101. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/base.py +0 -0
  102. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/gemini.py +0 -0
  103. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/groq.py +0 -0
  104. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/ollama.py +0 -0
  105. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/openai_compat.py +0 -0
  106. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/providers/openrouter.py +0 -0
  107. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/quiet.py +0 -0
  108. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/security.py +0 -0
  109. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/task_scheduler.py +0 -0
  110. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/telemetry_config.py +0 -0
  111. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/tool_registry.py +0 -0
  112. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/ui.py +0 -0
  113. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/core/visualizer.py +0 -0
  114. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/faults/__init__.py +0 -0
  115. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/faults/fault_detector.py +0 -0
  116. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/faults/fault_injector.py +0 -0
  117. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/parallel/__init__.py +0 -0
  118. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/parallel/executor.py +0 -0
  119. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/simulation/__init__.py +0 -0
  120. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/simulation/environment.py +0 -0
  121. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/simulation/real_perception.py +0 -0
  122. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/simulation/scenarios.py +0 -0
  123. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/skills/__init__.py +0 -0
  124. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/skills/drive_until_obstacle.py +0 -0
  125. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/skills/watch_distance.py +0 -0
  126. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/__init__.py +0 -0
  127. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/app.py +0 -0
  128. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/widgets/__init__.py +0 -0
  129. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/widgets/agent.py +0 -0
  130. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/widgets/common.py +0 -0
  131. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether/tui/widgets/doctor.py +0 -0
  132. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/SOURCES.txt +0 -0
  133. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/dependency_links.txt +0 -0
  134. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/entry_points.txt +0 -0
  135. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/requires.txt +0 -0
  136. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/aether_robotics.egg-info/top_level.txt +0 -0
  137. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/calibrated_camera_only_20260331_121709.json +0 -0
  138. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/calibrated_camera_only_20260331_122446.json +0 -0
  139. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/calibrated_camera_only_20260401_094544.json +0 -0
  140. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/calibrated_camera_only_20260401_094554.json +0 -0
  141. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/drone_v1.json +0 -0
  142. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/configs/rover_v1.json +0 -0
  143. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/context/aether_definitions.txt +0 -0
  144. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/install.sh +0 -0
  145. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/main.py +0 -0
  146. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/requirements.txt +0 -0
  147. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/setup.cfg +0 -0
  148. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_adapter_gpio_manifests.py +0 -0
  149. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_adapter_override_wiring.py +0 -0
  150. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_adapter_preflight.py +0 -0
  151. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_adapter_resolver_v397.py +0 -0
  152. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_analytics_cli.py +0 -0
  153. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_analytics_module.py +0 -0
  154. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_analytics_wiring.py +0 -0
  155. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_atexit_adapter_shutdown.py +0 -0
  156. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_atexit_reaches_stop_all.py +0 -0
  157. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_auto_adapter.py +0 -0
  158. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_auto_updater.py +0 -0
  159. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_cli.py +0 -0
  160. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_loop.py +0 -0
  161. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_loop_parallel.py +0 -0
  162. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_manifest_parity.py +0 -0
  163. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_autonomous_no_between_step_emergency_stop.py +0 -0
  164. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_boot_resolver_flow.py +0 -0
  165. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_calibrate_wizard.py +0 -0
  166. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_calibration.py +0 -0
  167. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_calibration_unit.py +0 -0
  168. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_chaos.py +0 -0
  169. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_doctor.py +0 -0
  170. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix.py +0 -0
  171. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix_patches.py +0 -0
  172. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix_v3_10_6.py +0 -0
  173. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix_v3_10_8.py +0 -0
  174. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_fix_visibility.py +0 -0
  175. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_kits.py +0 -0
  176. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_cli_routing.py +0 -0
  177. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_consent_flow.py +0 -0
  178. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_console_script_smoke.py +0 -0
  179. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_correction_agent.py +0 -0
  180. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_decomposer.py +0 -0
  181. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_decomposer_avoids_legacy.py +0 -0
  182. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_decomposer_reactive_routing.py +0 -0
  183. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_decomposer_routing.py +0 -0
  184. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_display_bug_fix.py +0 -0
  185. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_doctor_gpio_allocation.py +0 -0
  186. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_doctor_multi_adapter.py +0 -0
  187. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_drive_until_obstacle.py +0 -0
  188. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_embeddings.py +0 -0
  189. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_entrypoint.py +0 -0
  190. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_feedback_prompt.py +0 -0
  191. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_first_run_onboarding.py +0 -0
  192. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_fix_scope_validation.py +0 -0
  193. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_fix_sees_named_servos.py +0 -0
  194. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_genome.py +0 -0
  195. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_goal_persistence.py +0 -0
  196. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_gopigo3_adapter.py +0 -0
  197. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_gpio_conflict_detection.py +0 -0
  198. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_high_level_task_examples.py +0 -0
  199. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_install_environment.py +0 -0
  200. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_intent_category_fallback.py +0 -0
  201. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_json_repair_and_status.py +0 -0
  202. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_live_providers.py +0 -0
  203. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_map_room_high_level_task.py +0 -0
  204. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_mapper.py +0 -0
  205. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_mavlink_integration.py +0 -0
  206. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_memory_cli.py +0 -0
  207. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_memory_retrieval.py +0 -0
  208. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_migration.py +0 -0
  209. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_multi_adapter_boot_flow.py +0 -0
  210. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_multi_adapter_registration.py +0 -0
  211. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_multi_discovery.py +0 -0
  212. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_named_servo_registration.py +0 -0
  213. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_navigation_engine.py +0 -0
  214. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_oled_false_positive.py +0 -0
  215. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_overrides.py +0 -0
  216. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_overrides_multi_adapter.py +0 -0
  217. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_overrides_schema_v1_2.py +0 -0
  218. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_parallel_decomposer.py +0 -0
  219. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_parallel_executor.py +0 -0
  220. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_parallel_safety_hardware.py +0 -0
  221. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_parallel_validator.py +0 -0
  222. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_emergency_stop.py +0 -0
  223. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_emergency_stop_hardware.py +0 -0
  224. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_hard_off.py +0 -0
  225. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_pwm_primitives.py +0 -0
  226. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_pca9685_wake_sequence.py +0 -0
  227. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_persistent_memory.py +0 -0
  228. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_planner_routing.py +0 -0
  229. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_planner_stop_routing.py +0 -0
  230. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_planner_uses_retrieval.py +0 -0
  231. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_posthog_integration.py +0 -0
  232. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_primary_adapter_designation.py +0 -0
  233. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_provider_model_resolution.py +0 -0
  234. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_provider_outage_visibility.py +0 -0
  235. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_providers.py +0 -0
  236. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_quiet.py +0 -0
  237. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_robot_json_v1_1.py +0 -0
  238. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_security.py +0 -0
  239. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_security_v520.py +0 -0
  240. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_set_servo_angle_duration.py +0 -0
  241. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_stop_full_off_behavior.py +0 -0
  242. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_stop_servo_sleep_fallback.py +0 -0
  243. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_sunfounder_picarx_adapter.py +0 -0
  244. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_sunfounder_pidog_adapter.py +0 -0
  245. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_synthesizer_fidelity_check.py +0 -0
  246. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_task_scheduler.py +0 -0
  247. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tier1_motion_cleanup.py +0 -0
  248. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tool_builder.py +0 -0
  249. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tool_discovery.py +0 -0
  250. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tool_discovery_gpio_cleanup.py +0 -0
  251. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tool_namespacing.py +0 -0
  252. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tui_app.py +0 -0
  253. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tui_backward_compat.py +0 -0
  254. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_tui_widgets.py +0 -0
  255. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_ui_autonomous.py +0 -0
  256. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_ui_dashboard.py +0 -0
  257. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_ui_module.py +0 -0
  258. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_ui_wizard.py +0 -0
  259. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_v511_hardware_bugs.py +0 -0
  260. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_v511_ui.py +0 -0
  261. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_v512_feedback_routing.py +0 -0
  262. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_validator_new_tools.py +0 -0
  263. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_validator_rejects_legacy.py +0 -0
  264. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_watch_distance.py +0 -0
  265. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_yahboom_robot_hat_adapter.py +0 -0
  266. {aether_robotics-5.2.2 → aether_robotics-5.2.4}/tests/test_yolo_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aether-robotics
3
- Version: 5.2.2
3
+ Version: 5.2.4
4
4
  Summary: Autonomous multi-agent robotics system with DRL-First Hybrid FDIR
5
5
  Author: Chahel Paatur
6
6
  License: MIT
@@ -144,6 +144,20 @@ Full engineering detail, including the algorithms and where they came from: [doc
144
144
 
145
145
  ## What's New
146
146
 
147
+ ### v5.2.4 — The 22-second embedding load, and the warm-up that never ran
148
+
149
+ Loading MiniLM takes **~22 s on a Pi 4**. Everything else was noise.
150
+
151
+ - **The async warm-up had been dead since v5.1.0.** Its only call chain was `warm_up_async ← auto_migrate_on_boot ← print_startup`, and v5.1.0 moved `print_startup()` inside `if _verbose:` — so no normal boot warmed the model. It is now started explicitly from boot, nice'd, and instrumented (`[EMBED] warm-up loaded in 21840ms`).
152
+ - **`available()` was loading the model.** It was `get_model() is not None`, so asking "are embeddings available?" performed the full load — on every `plan_cache.enabled()`, and in the guard in front of the warm-up itself. One level down, `is_installed()` did `import sentence_transformers`, which pulls torch. Both are now genuinely cheap (`find_spec`, 30 ms).
153
+ - **Exact-match cache tier, no model required.** Repeating an objective verbatim — the common case — is now a normalized string comparison. Embedding similarity remains only as the fallback for genuinely reworded objectives.
154
+ - **`AETHER_RAG_MODE=auto|always|off`** (default `auto`): while the model is cold, retrieval is *skipped* and planning proceeds normally. An optimisation that costs 22 s to save 1 s must yield, not block.
155
+ - **`get_model()` is now lock-serialised.** Without it the main thread never waited for the warm-up thread — it started a *second* concurrent 22 s load.
156
+ - **`--objective` one-shot was broken**: it used the autonomous path, which has no plan-cache lookup, and discarded the result. It now checks the cache and prints the outcome.
157
+ - Never stall silently: `[MEM] Loading embedding model (first use, ~20s on a Pi)...`
158
+
159
+ **A correction:** the v5.2.2 and v5.2.3 latency claims were measured on a macOS dev box and extrapolated. The import-time work in v5.2.3 was real but was never the 50 s. This one was found by measuring on the hardware that had the problem.
160
+
147
161
  ### v5.2.2 — Boot is the latency now, so boot got measured
148
162
 
149
163
  With planning cached at 0 ms and `get_distance` at 6 ms, the remaining ~5 s was boot — running before every objective, and entirely unmeasured. Instrumented first:
@@ -187,6 +201,8 @@ Five sit above 0.88. And the discriminator isn't "contains a number" — *open*
187
201
 
188
202
  **Faster.** A repeat objective no longer calls the LLM at all. On real hardware `get_distance` executes in 4-6 ms while planning takes ~1 s, so for sensor-only work the model was ~99% of wall clock — and users repeat themselves constantly.
189
203
 
204
+ > **Note on the `[rag]` extra:** the embedding model costs **~22 s to load once per process on a Pi 4**. AETHER warms it in the background and, by default (`AETHER_RAG_MODE=auto`), skips retrieval entirely while it is still cold — so a cold first objective plans normally rather than waiting. Exact repeats of a previous objective are served from cache without touching the model at all.
205
+
190
206
  - **Semantic plan cache.** A successful plan is stored against the embedding of the objective that produced it; a semantically equivalent objective replays it. **~6 ms instead of ~1000 ms — roughly 158× on a repeat.** Every entry is keyed on the robot's genome hash and a mismatch is a *hard* miss: a cached plan is instructions for moving a physical machine and must never replay against different hardware. Parameters are replayed verbatim, never interpolated — 0.95 cosine still admits "2 seconds" vs "20 seconds". Needs the `[rag]` extra; degrades to normal planning without it.
191
207
  - **Anthropic prompt caching** on the static system prefix (~71 tool descriptions), plus **latency instrumentation** on every planning call: `[PLAN] via anthropic in 847ms (cached prefix: 4200 tok)`.
192
208
  - **Provider detection persisted** for 24 h against a credential-presence fingerprint, so boot stays off the detection path.
@@ -96,6 +96,20 @@ Full engineering detail, including the algorithms and where they came from: [doc
96
96
 
97
97
  ## What's New
98
98
 
99
+ ### v5.2.4 — The 22-second embedding load, and the warm-up that never ran
100
+
101
+ Loading MiniLM takes **~22 s on a Pi 4**. Everything else was noise.
102
+
103
+ - **The async warm-up had been dead since v5.1.0.** Its only call chain was `warm_up_async ← auto_migrate_on_boot ← print_startup`, and v5.1.0 moved `print_startup()` inside `if _verbose:` — so no normal boot warmed the model. It is now started explicitly from boot, nice'd, and instrumented (`[EMBED] warm-up loaded in 21840ms`).
104
+ - **`available()` was loading the model.** It was `get_model() is not None`, so asking "are embeddings available?" performed the full load — on every `plan_cache.enabled()`, and in the guard in front of the warm-up itself. One level down, `is_installed()` did `import sentence_transformers`, which pulls torch. Both are now genuinely cheap (`find_spec`, 30 ms).
105
+ - **Exact-match cache tier, no model required.** Repeating an objective verbatim — the common case — is now a normalized string comparison. Embedding similarity remains only as the fallback for genuinely reworded objectives.
106
+ - **`AETHER_RAG_MODE=auto|always|off`** (default `auto`): while the model is cold, retrieval is *skipped* and planning proceeds normally. An optimisation that costs 22 s to save 1 s must yield, not block.
107
+ - **`get_model()` is now lock-serialised.** Without it the main thread never waited for the warm-up thread — it started a *second* concurrent 22 s load.
108
+ - **`--objective` one-shot was broken**: it used the autonomous path, which has no plan-cache lookup, and discarded the result. It now checks the cache and prints the outcome.
109
+ - Never stall silently: `[MEM] Loading embedding model (first use, ~20s on a Pi)...`
110
+
111
+ **A correction:** the v5.2.2 and v5.2.3 latency claims were measured on a macOS dev box and extrapolated. The import-time work in v5.2.3 was real but was never the 50 s. This one was found by measuring on the hardware that had the problem.
112
+
99
113
  ### v5.2.2 — Boot is the latency now, so boot got measured
100
114
 
101
115
  With planning cached at 0 ms and `get_distance` at 6 ms, the remaining ~5 s was boot — running before every objective, and entirely unmeasured. Instrumented first:
@@ -139,6 +153,8 @@ Five sit above 0.88. And the discriminator isn't "contains a number" — *open*
139
153
 
140
154
  **Faster.** A repeat objective no longer calls the LLM at all. On real hardware `get_distance` executes in 4-6 ms while planning takes ~1 s, so for sensor-only work the model was ~99% of wall clock — and users repeat themselves constantly.
141
155
 
156
+ > **Note on the `[rag]` extra:** the embedding model costs **~22 s to load once per process on a Pi 4**. AETHER warms it in the background and, by default (`AETHER_RAG_MODE=auto`), skips retrieval entirely while it is still cold — so a cold first objective plans normally rather than waiting. Exact repeats of a previous objective are served from cache without touching the model at all.
157
+
142
158
  - **Semantic plan cache.** A successful plan is stored against the embedding of the objective that produced it; a semantically equivalent objective replays it. **~6 ms instead of ~1000 ms — roughly 158× on a repeat.** Every entry is keyed on the robot's genome hash and a mismatch is a *hard* miss: a cached plan is instructions for moving a physical machine and must never replay against different hardware. Parameters are replayed verbatim, never interpolated — 0.95 cosine still admits "2 seconds" vs "20 seconds". Needs the `[rag]` extra; degrades to normal planning without it.
143
159
  - **Anthropic prompt caching** on the static system prefix (~71 tool descriptions), plus **latency instrumentation** on every planning call: `[PLAN] via anthropic in 847ms (cached prefix: 4200 tok)`.
144
160
  - **Provider detection persisted** for 24 h against a credential-presence fingerprint, so boot stays off the detection path.
@@ -0,0 +1 @@
1
+ 5.2.4
@@ -1,4 +1,11 @@
1
- __version__ = "5.2.2"
1
+ import time as _time
2
+
3
+ #: Wall clock at the very first moment AETHER code runs. boot_timer measures
4
+ #: from here so "python_imports" is attributed rather than showing up as an
5
+ #: unexplained gap between the phase total and wall clock (v5.2.3).
6
+ _IMPORT_START = _time.time()
7
+
8
+ __version__ = "5.2.4"
2
9
  __author__ = "Chahel Paatur"
3
10
 
4
11
  #: Where users should actually be sent. v5.1.2: the GitHub repository is
@@ -907,6 +907,7 @@ def _extract_key_facts(output) -> str:
907
907
 
908
908
  def run_agent(args) -> None:
909
909
  """Real agent mode: prompt-driven loop with live tool execution."""
910
+ from aether.core import boot_timer
910
911
  from aether.core.analytics import check_consent_or_prompt
911
912
  check_consent_or_prompt() # v4.1.0.2: consent gate on every entrypoint
912
913
  from aether.core.tool_registry import ToolRegistry, ToolResult, register_built_tools
@@ -942,14 +943,15 @@ def run_agent(args) -> None:
942
943
  # ── Step 2: Banner already printed in main() ──────────────────────
943
944
 
944
945
  # ── Step 3: AutoUpdater ───────────────────────────────────────────
945
- updater = AutoUpdater()
946
+ with boot_timer.phase("autoupdater"):
947
+ updater = AutoUpdater()
946
948
  AutoUpdater.ensure_version_file(_PROJECT_ROOT)
947
949
  updater.run(auto_update=auto_update, no_update=no_update)
948
950
 
949
951
  # ── Step 4: Capability Discovery ──────────────────────────────────
950
- from aether.core import boot_timer
951
952
  discovery = ToolDiscovery()
952
- discovery.discover()
953
+ with boot_timer.phase("discovery"):
954
+ discovery.discover()
953
955
  # v5.1.1 UI 1: the ~70-line inventory is a debugging surface, not a boot
954
956
  # screen. It stays one `capabilities` command (or AETHER_VERBOSE=1) away,
955
957
  # and `aether doctor` is untouched.
@@ -957,8 +959,10 @@ def run_agent(args) -> None:
957
959
  discovery.print_summary()
958
960
 
959
961
  # ── Step 5: AutoInstaller ─────────────────────────────────────────
960
- installer = AutoInstaller(discovery.manifest)
961
- packages_installed = installer.run(auto_install=auto_install, no_install=no_install)
962
+ with boot_timer.phase("autoinstaller"):
963
+ installer = AutoInstaller(discovery.manifest)
964
+ packages_installed = installer.run(auto_install=auto_install,
965
+ no_install=no_install)
962
966
 
963
967
  # ── Step 6: Re-run discovery if anything was installed ────────────
964
968
  if packages_installed:
@@ -1080,8 +1084,9 @@ def run_agent(args) -> None:
1080
1084
  planner = Planner()
1081
1085
 
1082
1086
  # ── Step 7: Build Tools + Navigation Engine ───────────────────────
1083
- builder = ToolBuilder(discovery.manifest)
1084
- built_tools = builder.build_all()
1087
+ with boot_timer.phase("tool_build"):
1088
+ builder = ToolBuilder(discovery.manifest)
1089
+ built_tools = builder.build_all()
1085
1090
  _print_activity("TOOLS", builder.build_summary())
1086
1091
 
1087
1092
  # OLED startup animation (only on Pi with real hardware attached)
@@ -1093,7 +1098,8 @@ def run_agent(args) -> None:
1093
1098
  except Exception as e:
1094
1099
  _print_activity("OLED", f"startup animation failed: {e}")
1095
1100
 
1096
- nav_engine = NavigationEngine(discovery.manifest, tools=built_tools)
1101
+ with boot_timer.phase("nav_engine"):
1102
+ nav_engine = NavigationEngine(discovery.manifest, tools=built_tools)
1097
1103
  _register_gpio_cleanup(nav_engine) # drain PWMs + GPIO.cleanup on exit
1098
1104
  nav_actions = nav_engine.available_actions()
1099
1105
  _print_activity("NAV", f"Level {nav_engine.level} ({nav_engine.level_name}) — "
@@ -1218,6 +1224,19 @@ def run_agent(args) -> None:
1218
1224
  # Load persistent memory
1219
1225
  recent_memory = _load_agent_memory(5)
1220
1226
 
1227
+ # v5.2.4 (BUG 1): start the embedding warm-up EXPLICITLY. Its only call
1228
+ # chain was warm_up_async <- auto_migrate_on_boot <- print_startup, and
1229
+ # v5.1.0 moved print_startup() inside `if _verbose:` — so every normal
1230
+ # boot since has had no warm-up at all, and the first objective paid the
1231
+ # full ~22s model load on a Pi. Called here so it cannot be disconnected
1232
+ # again by a change to what boot prints.
1233
+ try:
1234
+ from aether.core import embeddings as _embeddings
1235
+ if _embeddings.available():
1236
+ _embeddings.warm_up_async()
1237
+ except Exception:
1238
+ pass
1239
+
1221
1240
  # v5.1.0 FIX 6: the 71-line tool dump is a debugging aid, not an
1222
1241
  # onboarding experience. It moves behind `tools` / --verbose, and a
1223
1242
  # first-time user gets three things they can actually type instead.
@@ -1260,7 +1279,10 @@ def run_agent(args) -> None:
1260
1279
  status = "OK" if mem.get("outcome") == "SUCCESS" else "FAIL"
1261
1280
  print(f" [{status}] {mem.get('objective', '?')[:60]}")
1262
1281
 
1263
- if _firstrun.check_and_mark(_firstrun.QUICKSTART_SHOWN):
1282
+ _is_one_shot = bool((getattr(args, "objective", "") or "").strip())
1283
+ if _is_one_shot:
1284
+ pass # v5.2.4: scripted use gets no REPL banner
1285
+ elif _firstrun.check_and_mark(_firstrun.QUICKSTART_SHOWN):
1264
1286
  _print_quickstart(available)
1265
1287
  else:
1266
1288
  print("\nType an objective ('tools' to list tools, "
@@ -1320,11 +1342,16 @@ def run_agent(args) -> None:
1320
1342
  # invocation must not touch shell history or block waiting on input.
1321
1343
  _one_shot = (getattr(args, "objective", "") or "").strip()
1322
1344
  if _one_shot:
1323
- _execute_objective(_one_shot, registry, llm_planner, corrector,
1324
- parser, manifest, _load_agent_memory(5),
1325
- available)
1345
+ # v5.2.4 (BUG 2): _execute_objective is the autonomous/TUI path and
1346
+ # has NO plan-cache lookup, so one-shot mode always paid a full API
1347
+ # call for an objective the REPL would have served from cache — and
1348
+ # its return value was discarded, so nothing was printed either. Route
1349
+ # through the shared helper the REPL uses instead.
1350
+ _result = _run_one_shot(_one_shot, registry, llm_planner, corrector,
1351
+ parser, manifest, _load_agent_memory(5),
1352
+ available)
1326
1353
  _feedback.session_ended()
1327
- return
1354
+ sys.exit(0 if _result.get("status") in ("SUCCESS", "DEGRADED") else 1)
1328
1355
 
1329
1356
  _ui.enable_readline() # v5.1.1 UI 2: history + line editing
1330
1357
 
@@ -1397,10 +1424,6 @@ def run_agent(args) -> None:
1397
1424
  plan = None
1398
1425
  plan_source = "keyword"
1399
1426
 
1400
- # Inject planning hints from past experience
1401
- hints = _persistent_memory.planning_hints(objective)
1402
- if hints:
1403
- _print_activity("MEM", "Found similar past objectives")
1404
1427
 
1405
1428
  if oled_tool is not None:
1406
1429
  try:
@@ -1413,7 +1436,19 @@ def run_agent(args) -> None:
1413
1436
  # the genome hash — a plan built for one robot must never run on
1414
1437
  # another, so a hash mismatch is a hard miss, not a fuzzy match.
1415
1438
  from aether.core import plan_cache as _plan_cache
1416
- _cache_hit = _plan_cache.lookup(objective, _genome_hash(), available)
1439
+ # v5.2.4 (BUG 1): the cache is consulted FIRST. On a hit the planner
1440
+ # is never called, so building its retrieval context beforehand was
1441
+ # pure waste — and on a cold model it was a ~22s stall to produce
1442
+ # hints nothing would read.
1443
+ _cache_hit = (_plan_cache.lookup_exact(objective, _genome_hash(),
1444
+ available)
1445
+ or _plan_cache.lookup(objective, _genome_hash(),
1446
+ available))
1447
+ hints = None
1448
+ if _cache_hit is None:
1449
+ hints = _persistent_memory.planning_hints(objective)
1450
+ if hints:
1451
+ _print_activity("MEM", "Found similar past objectives")
1417
1452
  if _cache_hit is not None:
1418
1453
  _entry, _similarity = _cache_hit
1419
1454
  plan = [dict(step) for step in _entry["plan"]]
@@ -2301,6 +2336,57 @@ def _summarize_skill_output(output) -> Optional[str]:
2301
2336
  return None
2302
2337
 
2303
2338
 
2339
+ def _run_one_shot(objective, registry, llm_planner, corrector, parser,
2340
+ manifest, recent_memory, available):
2341
+ """Execute one objective and PRINT what happened (v5.2.4 BUG 2).
2342
+
2343
+ Shares _execute_objective with autonomous mode, but adds the two things
2344
+ one-shot mode was missing: a plan-cache lookup before planning, and
2345
+ printing the result rather than exiting silently.
2346
+ """
2347
+ from aether.core import plan_cache as _plan_cache
2348
+
2349
+ hit = (_plan_cache.lookup_exact(objective, _genome_hash(), available)
2350
+ or _plan_cache.lookup(objective, _genome_hash(), available))
2351
+ if hit is not None:
2352
+ entry, similarity = hit
2353
+ print(f" [CACHE] Reusing plan (similarity {similarity:.2f}) — "
2354
+ f"skipped the planning call")
2355
+ llm_planner = _CachedPlanner(entry["plan"], llm_planner)
2356
+
2357
+ result = _execute_objective(objective, registry, llm_planner, corrector,
2358
+ parser, manifest, recent_memory, available)
2359
+
2360
+ outcome = result.get("status", "?")
2361
+ print(f"\n {'✓' if outcome == 'SUCCESS' else '!'} {outcome} · "
2362
+ f"{len(result.get('actions', []))} action(s) · "
2363
+ f"{result.get('faults', 0)} fault(s)")
2364
+ payload = result.get("result")
2365
+ if payload:
2366
+ text = _summarize_output(payload)
2367
+ print(f" {text}")
2368
+ return result
2369
+
2370
+
2371
+ class _CachedPlanner:
2372
+ """Serves a cached plan in place of an API call, keeping the planner's
2373
+ interface so _execute_objective is unchanged."""
2374
+
2375
+ def __init__(self, plan, wrapped):
2376
+ self._plan = [dict(step) for step in plan]
2377
+ self._wrapped = wrapped
2378
+ self.last_raw_response = ""
2379
+ self.last_provider_error = ""
2380
+ self.last_latency_ms = 0.0
2381
+
2382
+ @property
2383
+ def available(self):
2384
+ return True
2385
+
2386
+ def plan(self, objective, manifest, memory=None):
2387
+ return self._plan
2388
+
2389
+
2304
2390
  def _execute_objective(objective: str, registry, llm_planner, corrector,
2305
2391
  parser_obj, manifest, recent_memory,
2306
2392
  available, fix_mode: bool = False,
@@ -87,6 +87,26 @@ class AutoUpdater:
87
87
  pass
88
88
  return CURRENT_VERSION
89
89
 
90
+ #: v5.2.3: the update check is CACHED for a day. It used to hit PyPI on
91
+ #: every boot — network I/O on the critical path, which is exactly the
92
+ #: user>>real gap that made a 50s boot only 21s of CPU. A version check is
93
+ #: not worth a single millisecond of a robot's startup.
94
+ _CHECK_TTL_S = 24 * 3600
95
+
96
+ def _cached_check(self):
97
+ try:
98
+ from aether.core.providers._cache import cache_get
99
+ return cache_get("update:latest", self._CHECK_TTL_S)
100
+ except Exception:
101
+ return None
102
+
103
+ def _remember_check(self, payload) -> None:
104
+ try:
105
+ from aether.core.providers._cache import cache_set
106
+ cache_set("update:latest", payload)
107
+ except Exception:
108
+ pass
109
+
90
110
  def _fetch_latest_release(self) -> Optional[Dict]:
91
111
  """Latest published version, from PyPI.
92
112
 
@@ -98,6 +118,11 @@ class AutoUpdater:
98
118
 
99
119
  Returns the same {"tag_name", "body"} shape the callers already expect.
100
120
  """
121
+ cached = self._cached_check()
122
+ if isinstance(cached, dict) and cached.get("tag_name"):
123
+ return cached
124
+ if os.environ.get("AETHER_NO_UPDATE_CHECK"):
125
+ return None
101
126
  try:
102
127
  import urllib.request
103
128
  with urllib.request.urlopen(_PYPI_JSON_URL,
@@ -108,8 +133,11 @@ class AutoUpdater:
108
133
  version = str(info.get("version") or "").strip()
109
134
  if not version:
110
135
  return None
111
- return {"tag_name": f"v{version}",
112
- "body": f"See {info.get('project_url') or 'PyPI'} for details."}
136
+ payload = {"tag_name": f"v{version}",
137
+ "body": f"See {info.get('project_url') or 'PyPI'} "
138
+ f"for details."}
139
+ self._remember_check(payload)
140
+ return payload
113
141
 
114
142
  def check_for_update(self) -> Optional[str]:
115
143
  """Check GitHub releases for a newer version.
@@ -75,6 +75,56 @@ def summary_line(limit: int = 6) -> str:
75
75
  return " · ".join(parts) + f" · total {total_ms():.0f}ms"
76
76
 
77
77
 
78
+ #: Wall clock at interpreter start, so the report covers process start to
79
+ #: prompt rather than only the phases someone remembered to wrap.
80
+ try:
81
+ from aether import _IMPORT_START as _PROCESS_START
82
+ except Exception:
83
+ _PROCESS_START = time.time()
84
+
85
+ #: The measured phases must account for at least this fraction of wall clock.
86
+ #: v5.2.2 shipped a timer that wrapped ONE function and then printed
87
+ #: "total 25ms" for a 50,845ms boot — a claim far worse than no instrumentation,
88
+ #: because it said the problem was solved. If coverage is below this, the
89
+ #: report says so instead of quoting a total it cannot justify.
90
+ COVERAGE_FLOOR = 0.95
91
+
92
+
93
+ def wall_ms() -> float:
94
+ return (time.time() - _PROCESS_START) * 1000.0
95
+
96
+
97
+ def coverage() -> float:
98
+ """Fraction of wall clock the measured phases actually account for."""
99
+ wall = wall_ms()
100
+ return (total_ms() / wall) if wall > 0 else 0.0
101
+
102
+
103
+ def coverage_note() -> str:
104
+ """'(wall 50845ms ✓)' or a loud warning that the numbers do not sum."""
105
+ wall = wall_ms()
106
+ if coverage() >= COVERAGE_FLOOR:
107
+ return f"(wall {wall:.0f}ms ✓)"
108
+ unmeasured = wall - total_ms()
109
+ return (f"(wall {wall:.0f}ms — ⚠ {unmeasured:.0f}ms UNMEASURED, "
110
+ f"phases cover only {coverage() * 100:.0f}%)")
111
+
112
+
113
+ def record_import_phase() -> None:
114
+ """Attribute everything before the first timed phase to python_imports.
115
+
116
+ Without this the phases never sum to wall clock and the coverage check
117
+ warns forever, which would train everyone to ignore it — the same way the
118
+ v5.2.2 "total 25ms" line trained us to believe boot was fast.
119
+ """
120
+ if not _enabled or any(p["phase"] == "python_imports" for p in _phases):
121
+ return
122
+ already = total_ms()
123
+ before = wall_ms() - already
124
+ if before > 0:
125
+ _phases.insert(0, {"phase": "python_imports", "ms": round(before, 1)})
126
+
127
+
78
128
  def report() -> None:
79
129
  """Print the summary under AETHER_VERBOSE=1 and always emit analytics.
80
130
 
@@ -84,9 +134,13 @@ def report() -> None:
84
134
  """
85
135
  if not _phases:
86
136
  return
137
+ record_import_phase()
87
138
  try:
88
139
  if os.environ.get("AETHER_VERBOSE"):
89
- print(f" [BOOT] {summary_line()}")
140
+ for entry in sorted(_phases, key=lambda p: -p["ms"]):
141
+ print(f" [BOOT] {entry['phase']:<24} {entry['ms']:>8.0f}ms")
142
+ print(f" [BOOT] {'TOTAL':<24} {total_ms():>8.0f}ms "
143
+ f"{coverage_note()}")
90
144
  except Exception:
91
145
  pass
92
146
  try:
@@ -94,5 +148,9 @@ def report() -> None:
94
148
  for entry in _phases:
95
149
  track("boot_latency", {"phase": entry["phase"], "ms": entry["ms"]})
96
150
  track("boot_latency", {"phase": "total", "ms": total_ms()})
151
+ # Coverage travels with the numbers: a total that accounts for 3% of
152
+ # wall clock must not be read as a fast boot.
153
+ track("boot_latency", {"phase": "wall", "ms": round(wall_ms(), 1),
154
+ "coverage": round(coverage(), 3)})
97
155
  except Exception:
98
156
  pass
@@ -55,14 +55,54 @@ def reset_model() -> None:
55
55
 
56
56
 
57
57
  def is_installed() -> bool:
58
- """True when the sentence-transformers package can be imported."""
58
+ """True when sentence-transformers is present — WITHOUT importing it.
59
+
60
+ v5.2.4: this used to `import sentence_transformers`, which pulls torch —
61
+ ~0.6s on a dev Mac and 10-20s on a Pi 4. So the cheap-looking capability
62
+ check was itself the expensive thing, one level below available(). find_spec
63
+ locates the package without executing it.
64
+ """
59
65
  try:
60
- import sentence_transformers # noqa: F401
61
- return True
66
+ import importlib.util
67
+ return importlib.util.find_spec("sentence_transformers") is not None
62
68
  except Exception:
63
69
  return False
64
70
 
65
71
 
72
+ _LOAD_LOCK = None
73
+ _warm_started = 0.0
74
+ _warm_elapsed = None
75
+
76
+
77
+ def _lock():
78
+ """One lock, created lazily so import stays cheap."""
79
+ global _LOAD_LOCK
80
+ if _LOAD_LOCK is None:
81
+ import threading
82
+ _LOAD_LOCK = threading.Lock()
83
+ return _LOAD_LOCK
84
+
85
+
86
+ def is_loaded() -> bool:
87
+ """True when the model is resident and embedding is effectively free.
88
+
89
+ v5.2.4: callers use this to DECIDE whether an optional embedding is worth
90
+ it. On a Pi 4 a cold load is ~22s, so retrieval — an optimisation for a
91
+ ~1s planning call — must never be the thing that blocks.
92
+ """
93
+ return _model is not None
94
+
95
+
96
+ def warm_state() -> dict:
97
+ """Diagnostics for the warm-up thread (v5.2.4)."""
98
+ import threading
99
+ alive = any(t.name == "aether-embed-warmup" and t.is_alive()
100
+ for t in threading.enumerate())
101
+ return {"loaded": is_loaded(), "failed": _load_failed,
102
+ "thread_alive": alive, "started_at": _warm_started,
103
+ "elapsed_ms": _warm_elapsed}
104
+
105
+
66
106
  def get_model():
67
107
  """Return the cached SentenceTransformer, loading it on first use.
68
108
 
@@ -74,6 +114,27 @@ def get_model():
74
114
  return _model
75
115
  if _load_failed:
76
116
  return None
117
+
118
+ # v5.2.4: without this lock the main thread did NOT wait for the warm-up
119
+ # thread — it saw _model is None and started a SECOND concurrent load.
120
+ # Two 22-second torch loads competing for four Pi cores is worse than one.
121
+ with _lock():
122
+ if _model is not None:
123
+ return _model
124
+ if _load_failed:
125
+ return None
126
+ # v5.2.4: fourteen silent seconds reads as a hang. Announced on the
127
+ # foreground path only — the warm-up thread has its own [EMBED] line
128
+ # and should not print over the boot screen.
129
+ import threading as _th
130
+ if _th.current_thread().name != "aether-embed-warmup":
131
+ print(" [MEM] Loading embedding model "
132
+ "(first use, ~20s on a Pi)...", flush=True)
133
+ return _load_model_locked()
134
+
135
+
136
+ def _load_model_locked():
137
+ global _model, _load_failed
77
138
  try:
78
139
  # Keep HF quiet: progress bars and telemetry would corrupt the v4.2.0
79
140
  # TUI screen and add noise to classic CLI output.
@@ -106,8 +167,24 @@ def get_model():
106
167
 
107
168
 
108
169
  def available() -> bool:
109
- """True when embeddings can actually be computed right now."""
110
- return get_model() is not None
170
+ """True when embeddings COULD be computed — without loading the model.
171
+
172
+ v5.2.4: this used to be `get_model() is not None`, i.e. asking "are
173
+ embeddings available?" performed the full ~22s load on a Pi. Every caller
174
+ that thought it was making a cheap capability check was paying for it,
175
+ including plan_cache.enabled() on every objective and the guard in front
176
+ of warm_up_async — which meant deciding whether to warm the model
177
+ asynchronously first loaded it synchronously.
178
+
179
+ Availability is now package-installability plus "we have not already
180
+ failed", which is what callers actually meant. Use is_loaded() to ask
181
+ whether embedding is free RIGHT NOW.
182
+ """
183
+ if _load_failed:
184
+ return False
185
+ if _model is not None:
186
+ return True
187
+ return is_installed()
111
188
 
112
189
 
113
190
  # Single source of truth for "why is retrieval off?" so every surface gives
@@ -143,14 +220,34 @@ def warm_up_async() -> None:
143
220
  import threading
144
221
 
145
222
  def _load():
223
+ global _warm_elapsed
224
+ import time as _t
225
+ start = _t.time()
146
226
  try:
227
+ # v5.2.4: nice the thread. The load is ~22s of CPU-bound torch
228
+ # work; on a 4-core Pi at default priority it contends with the
229
+ # boot path it is supposed to be hiding behind.
230
+ try:
231
+ os.nice(10)
232
+ except Exception:
233
+ pass
147
234
  get_model()
148
235
  except Exception:
149
236
  pass
237
+ finally:
238
+ _warm_elapsed = round((_t.time() - start) * 1000.0, 1)
239
+ if os.environ.get("AETHER_VERBOSE"):
240
+ state = "loaded" if is_loaded() else "FAILED"
241
+ print(f" [EMBED] warm-up {state} in {_warm_elapsed:.0f}ms")
150
242
 
243
+ global _warm_started
151
244
  try:
245
+ import time as _t
246
+ _warm_started = _t.time()
152
247
  threading.Thread(target=_load, name="aether-embed-warmup",
153
248
  daemon=True).start()
249
+ if os.environ.get("AETHER_VERBOSE"):
250
+ print(" [EMBED] warm-up thread started")
154
251
  except Exception:
155
252
  pass
156
253
 
@@ -561,7 +561,23 @@ def build_retrieval_block(objective: str, k: int = 3,
561
561
  unavailable, or anything at all goes wrong — callers append the result
562
562
  unconditionally, so an empty string means the prompt is byte-identical
563
563
  to v4.2.0. Never injects empty scaffolding.
564
+
565
+ v5.2.4: in AETHER_RAG_MODE=auto (the default) this returns "" while the
566
+ embedding model is still cold. Retrieval improves a ~1s planning call; on
567
+ a Pi 4 loading MiniLM costs ~22s. An optimisation that is twenty times
568
+ more expensive than what it optimises must yield, not block.
564
569
  """
570
+ try:
571
+ from aether.core.plan_cache import embedding_ready, rag_mode
572
+ if not embedding_ready():
573
+ if rag_mode() == "auto":
574
+ import os as _os
575
+ if _os.environ.get("AETHER_VERBOSE"):
576
+ print(" [MEM] Embedding model still loading — "
577
+ "planning without retrieval this time")
578
+ return ""
579
+ except Exception:
580
+ pass
565
581
  try:
566
582
  # Resolve the default path at call time (not via the __init__ default
567
583
  # arg, which binds at import) so tests can redirect it.
@@ -149,22 +149,44 @@ class _CameraNav:
149
149
  cv2 is not installed.
150
150
  """
151
151
 
152
+ @property
153
+ def _backend_name(self):
154
+ self._resolve_nav_backend()
155
+ return self._backend_name_cached
156
+
157
+ @property
158
+ def _backend_mod(self):
159
+ self._resolve_nav_backend()
160
+ return self._backend_mod_cached
161
+
162
+ def _resolve_nav_backend(self):
163
+ if self._nav_backend_resolved:
164
+ return
165
+ self._nav_backend_resolved = True
166
+ (self._backend_name_cached,
167
+ self._backend_mod_cached) = _detect_nav_camera_backend()
168
+ # Logged here, not in __init__: reading the backend just to name it
169
+ # was what imported cv2 on every boot.
170
+ if self._backend_name_cached == "cv2":
171
+ self._cv2 = self._backend_mod_cached
172
+ _nav_cam_log.info("Using cv2 backend")
173
+ elif self._backend_name_cached == "picamera2":
174
+ _nav_cam_log.info("Using picamera2 backend")
175
+ else:
176
+ _nav_cam_log.warning("No camera backend available")
177
+
152
178
  def __init__(self):
153
- self._backend_name, self._backend_mod = _detect_nav_camera_backend()
179
+ # v5.2.3: deferred — this imported cv2 on every boot for a camera the
180
+ # session may never use.
181
+ self._backend_name_cached = None
182
+ self._backend_mod_cached = None
183
+ self._nav_backend_resolved = False
154
184
  self._cv2 = None
155
185
  self._cap = None # cv2 VideoCapture
156
186
  self._picam = None # picamera2 instance
157
187
  self._picam_started = False
158
188
  self._prev_gray = None
159
189
 
160
- if self._backend_name == "cv2":
161
- self._cv2 = self._backend_mod
162
- _nav_cam_log.info("Using cv2 backend")
163
- elif self._backend_name == "picamera2":
164
- _nav_cam_log.info("Using picamera2 backend")
165
- else:
166
- _nav_cam_log.warning("No camera backend available")
167
-
168
190
  atexit.register(self.close)
169
191
 
170
192
  def _ensure_picamera(self) -> Optional[str]: