devops-bot-sdk 1.6.70__tar.gz → 1.6.71__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 (236) hide show
  1. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
  4. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/settings.py +21 -1
  6. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/shipper.py +31 -4
  7. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/spool.py +51 -16
  8. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_activity_collectors.py +8 -1
  9. devops_bot_sdk-1.6.71/tests/test_activity_live_tracking.py +145 -0
  10. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_activity_macos.py +8 -1
  11. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_activity_pipeline.py +18 -14
  12. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/README.md +0 -0
  13. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  14. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  15. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/requires.txt +0 -0
  16. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  17. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/pyproject.toml +0 -0
  18. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/__init__.py +0 -0
  19. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/classifier.py +0 -0
  20. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/__init__.py +0 -0
  21. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/common.py +0 -0
  22. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/linux_auditd.py +0 -0
  23. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/macos_eslogger.py +0 -0
  24. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/windows_eventlog.py +0 -0
  25. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/models.py +0 -0
  26. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/os_setup.py +0 -0
  27. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/recorder.py +0 -0
  28. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/activity/redact.py +0 -0
  29. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agentd.py +0 -0
  30. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/__init__.py +0 -0
  31. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/activity.py +0 -0
  32. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/builder.py +0 -0
  33. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/chat.py +0 -0
  34. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/connectors.py +0 -0
  35. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/engines/__init__.py +0 -0
  36. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/engines/claude_code.py +0 -0
  37. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/engines/http.py +0 -0
  38. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/engines/mcp.py +0 -0
  39. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/engines/n8n.py +0 -0
  40. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/engines/shell.py +0 -0
  41. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/integrations.py +0 -0
  42. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/inventory.py +0 -0
  43. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/library/__init__.py +0 -0
  44. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/manifest.py +0 -0
  45. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/prompt_studio.py +0 -0
  46. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/prompt_template.py +0 -0
  47. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/prompt_vars.py +0 -0
  48. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/run_log.py +0 -0
  49. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/schedules.py +0 -0
  50. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/secrets.py +0 -0
  51. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/ticket_run.py +0 -0
  52. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/usage.py +0 -0
  53. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/agents/varstore.py +0 -0
  54. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/browser_profile.py +0 -0
  55. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/cli.py +0 -0
  56. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/client.py +0 -0
  57. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/collectors/__init__.py +0 -0
  58. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/collectors/files.py +0 -0
  59. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/collectors/process.py +0 -0
  60. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/collectors/screenshot.py +0 -0
  61. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/config.py +0 -0
  62. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/crucial.py +0 -0
  63. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/custom_agents.py +0 -0
  64. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/departments.py +0 -0
  65. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/design_verify/__init__.py +0 -0
  66. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/design_verify/assets.py +0 -0
  67. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/design_verify/bootstrap.py +0 -0
  68. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/design_verify/browser.py +0 -0
  69. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/design_verify/compare.py +0 -0
  70. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/design_verify/loop.py +0 -0
  71. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/estimator.py +0 -0
  72. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/exceptions.py +0 -0
  73. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/firewall.py +0 -0
  74. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/formats.py +0 -0
  75. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/git_ops.py +0 -0
  76. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/github_fetch.py +0 -0
  77. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/github_ingest.py +0 -0
  78. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/graph_recon.py +0 -0
  79. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/graphify.py +0 -0
  80. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/hooks/__init__.py +0 -0
  81. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/hooks/crucial_guard.py +0 -0
  82. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/intake.py +0 -0
  83. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ipc/__init__.py +0 -0
  84. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ipc/electron_bridge.py +0 -0
  85. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ipc/handlers.py +0 -0
  86. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/journal.py +0 -0
  87. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/local_exec.py +0 -0
  88. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/memory_guard.py +0 -0
  89. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/models/__init__.py +0 -0
  90. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/models/envelope.py +0 -0
  91. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/models/requests.py +0 -0
  92. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/models/responses.py +0 -0
  93. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/models/snapshots.py +0 -0
  94. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/phase_timer.py +0 -0
  95. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/plan_run.py +0 -0
  96. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/platform_compat.py +0 -0
  97. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/prd_progress.py +0 -0
  98. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/proof_readers.py +0 -0
  99. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/providers.py +0 -0
  100. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/py.typed +0 -0
  101. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/resume.py +0 -0
  102. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/router.py +0 -0
  103. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/run_auto.py +0 -0
  104. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/schedule_tickets.py +0 -0
  105. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/scheduler.py +0 -0
  106. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/self_report.py +0 -0
  107. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/self_report_local.py +0 -0
  108. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/sse.py +0 -0
  109. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/swagger.py +0 -0
  110. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/task_file.py +0 -0
  111. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/task_types.py +0 -0
  112. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/test.py +0 -0
  113. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/test_pipeline.py +0 -0
  114. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ticket_fields.py +0 -0
  115. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/tickets.py +0 -0
  116. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/tiers/__init__.py +0 -0
  117. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/tiers/mini_agent.py +0 -0
  118. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/__init__.py +0 -0
  119. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/api.py +0 -0
  120. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/dashboard.py +0 -0
  121. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/health.py +0 -0
  122. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/ip_allowlist.py +0 -0
  123. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/jobs.py +0 -0
  124. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/models.py +0 -0
  125. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/runner.py +0 -0
  126. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/security.py +0 -0
  127. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/server.py +0 -0
  128. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/service.py +0 -0
  129. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/session.py +0 -0
  130. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/setup_manager.py +0 -0
  131. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/agents.html +0 -0
  132. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/app.js +0 -0
  133. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/chat.html +0 -0
  134. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/custom-agents.html +0 -0
  135. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/default-agents.html +0 -0
  136. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/index.html +0 -0
  137. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/report.html +0 -0
  138. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/runs.html +0 -0
  139. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/schedules.html +0 -0
  140. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/setup.html +0 -0
  141. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/styles.css +0 -0
  142. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/static/tickets.html +0 -0
  143. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/ui/swagger.py +0 -0
  144. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/updater.py +0 -0
  145. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/url_access.py +0 -0
  146. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  147. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/vps_health.py +0 -0
  148. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/vps_identity.py +0 -0
  149. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/sdk/write_hook.py +0 -0
  150. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/setup.cfg +0 -0
  151. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_activity_core.py +0 -0
  152. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_activity_os_setup.py +0 -0
  153. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_activity.py +0 -0
  154. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_activity_tracking.py +0 -0
  155. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_chat_features.py +0 -0
  156. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_crud_cli.py +0 -0
  157. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_listing.py +0 -0
  158. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_prompt_async.py +0 -0
  159. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_prompt_models.py +0 -0
  160. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_run_ticket.py +0 -0
  161. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_schedules.py +0 -0
  162. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agent_workspace_settings.py +0 -0
  163. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_agents_connectors_builder.py +0 -0
  164. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_allowed_tools_config.py +0 -0
  165. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_auto_agent_dispatch.py +0 -0
  166. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_browser_profile.py +0 -0
  167. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_cheap_tier.py +0 -0
  168. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_client_jira_projects.py +0 -0
  169. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_config_token_diagnostics.py +0 -0
  170. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_configure_token_owner.py +0 -0
  171. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_console_encoding.py +0 -0
  172. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_crash_recovery.py +0 -0
  173. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_departments.py +0 -0
  174. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_done_transition.py +0 -0
  175. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_epic_decompose_run.py +0 -0
  176. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_epic_inherited_agent_id.py +0 -0
  177. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_epic_session_isolation.py +0 -0
  178. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_estimator.py +0 -0
  179. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_fallback_cli.py +0 -0
  180. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_firewall_open_on_update.py +0 -0
  181. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_formats.py +0 -0
  182. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_git_action.py +0 -0
  183. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_github_fetch.py +0 -0
  184. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_github_ingest.py +0 -0
  185. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_graph_recon.py +0 -0
  186. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_graphify_build.py +0 -0
  187. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_graphify_hook_install.py +0 -0
  188. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_health.py +0 -0
  189. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_intake_job_outcome.py +0 -0
  190. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_intake_plan_review.py +0 -0
  191. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_journal.py +0 -0
  192. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_local_exec_provider.py +0 -0
  193. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_mcp_route_contract.py +0 -0
  194. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_memory_guard.py +0 -0
  195. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_phase_timer.py +0 -0
  196. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_plan_new_command.py +0 -0
  197. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_plan_run_activity.py +0 -0
  198. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_pr_identity.py +0 -0
  199. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_prd_artifact.py +0 -0
  200. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_project_path_resolution.py +0 -0
  201. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_proof_readers.py +0 -0
  202. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_providers.py +0 -0
  203. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_recon_session_isolation.py +0 -0
  204. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_resume.py +0 -0
  205. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_router.py +0 -0
  206. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_run_auto_agent_only.py +0 -0
  207. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_run_outputs.py +0 -0
  208. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_run_verdict_is_honest.py +0 -0
  209. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_schedule_api.py +0 -0
  210. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_schedule_outputs_and_plans.py +0 -0
  211. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_schedule_tickets.py +0 -0
  212. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_scheduler.py +0 -0
  213. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_scheduler_transient_retry.py +0 -0
  214. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_self_report.py +0 -0
  215. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_service_token_warning.py +0 -0
  216. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_stale_panel_restart.py +0 -0
  217. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_swagger_command.py +0 -0
  218. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_task_file_and_plan_run.py +0 -0
  219. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_task_file_format_doc.py +0 -0
  220. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_ticket_cli.py +0 -0
  221. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_ticket_fields.py +0 -0
  222. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_ticket_run_resolve_by_id.py +0 -0
  223. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_ticket_speed_fixes.py +0 -0
  224. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_tier_flag_parity.py +0 -0
  225. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_ui_gate.py +0 -0
  226. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_ui_lifespan.py +0 -0
  227. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_update_timer.py +0 -0
  228. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_updater_extras.py +0 -0
  229. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_updater_pep668.py +0 -0
  230. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_updater_reports_the_truth.py +0 -0
  231. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_url_access.py +0 -0
  232. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_vps_health.py +0 -0
  233. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_vps_only_reporting.py +0 -0
  234. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_windows_loop_identity.py +0 -0
  235. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_windows_task_paths.py +0 -0
  236. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.71}/tests/test_write_hook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.70
3
+ Version: 1.6.71
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.70
3
+ Version: 1.6.71
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -144,6 +144,7 @@ sdk/ui/static/tickets.html
144
144
  sdk/vendor/aidraftsman-cli-0.1.0.tgz
145
145
  tests/test_activity_collectors.py
146
146
  tests/test_activity_core.py
147
+ tests/test_activity_live_tracking.py
147
148
  tests/test_activity_macos.py
148
149
  tests/test_activity_os_setup.py
149
150
  tests/test_activity_pipeline.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.70
3
+ Version: 1.6.71
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -34,7 +34,7 @@ Rules:
34
34
  - All data egress through submit_webhook only
35
35
  """
36
36
 
37
- __version__ = "1.6.70" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.71" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
38
38
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
39
39
 
40
40
  from sdk.client import BackendClient
@@ -23,8 +23,12 @@ from sdk.activity.spool import Spool
23
23
 
24
24
  logger = logging.getLogger("sdk.activity.settings")
25
25
 
26
- DEFAULT_SHIP_INTERVAL_S = 300.0
26
+ # Live tracking: one POST a minute is far under the relay's 60/min budget, and the
27
+ # five-minute default meant the newest activity was always up to five minutes stale.
28
+ DEFAULT_SHIP_INTERVAL_S = 60.0
27
29
  MIN_SHIP_INTERVAL_S = 30.0
30
+ #: Events older than this are discarded instead of shipped (``0`` keeps everything).
31
+ DEFAULT_MAX_AGE_S = 3600.0
28
32
  _FALSEY = {"0", "false", "no", "off"}
29
33
  _TRUTHY = {"1", "true", "yes", "on"}
30
34
 
@@ -63,6 +67,22 @@ def ship_interval_seconds() -> float:
63
67
  return DEFAULT_SHIP_INTERVAL_S
64
68
 
65
69
 
70
+ def max_age_seconds() -> float:
71
+ """``AGENTOS_ACTIVITY_MAX_AGE_S`` — how old an event may be and still ship.
72
+
73
+ Default one hour: the trail is for live tracking, and a backlog shipped
74
+ oldest-first kept busy hosts days behind. ``0`` disables the cut-off.
75
+ """
76
+ raw = setting("AGENTOS_ACTIVITY_MAX_AGE_S")
77
+ if not raw:
78
+ return DEFAULT_MAX_AGE_S
79
+ try:
80
+ return max(0.0, float(raw))
81
+ except ValueError:
82
+ logger.warning("AGENTOS_ACTIVITY_MAX_AGE_S=%r is not a number; using default", raw)
83
+ return DEFAULT_MAX_AGE_S
84
+
85
+
66
86
  def spool_path() -> Path:
67
87
  """``~/.agentos/activity.db`` — resolved at call time so tests can redirect HOME."""
68
88
  return Path.home() / ".agentos" / "activity.db"
@@ -1,4 +1,4 @@
1
- """Ship the activity spool to devops-bot-ai every 5 minutes.
1
+ """Ship the activity spool to devops-bot-ai every minute, newest first.
2
2
 
3
3
  Same shape as ``sdk.vps_health``: an asyncio loop with a ``start()`` the UI
4
4
  server's lifespan calls, cancelled by the host on shutdown.
@@ -25,8 +25,10 @@ from __future__ import annotations
25
25
 
26
26
  import asyncio
27
27
  import logging
28
+ import time
28
29
  from collections.abc import Awaitable, Callable
29
30
  from dataclasses import dataclass
31
+ from datetime import datetime, timezone
30
32
  from typing import Any, Protocol
31
33
 
32
34
  from sdk.activity import settings
@@ -188,17 +190,42 @@ def discard_spool(spool: Spool | None = None) -> int:
188
190
  spool.ack(chunk)
189
191
 
190
192
 
193
+ def purge_stale_rows(spool: Spool | None = None, now: float | None = None) -> int:
194
+ """Discard what is older than ``settings.max_age_seconds()``. Returns rows removed."""
195
+ max_age = settings.max_age_seconds()
196
+ if max_age <= 0:
197
+ return 0
198
+ spool = spool or settings.get_spool()
199
+ cutoff = (now if now is not None else time.time()) - max_age
200
+ cutoff_iso = (datetime.fromtimestamp(cutoff, timezone.utc)
201
+ .isoformat(timespec="milliseconds").replace("+00:00", "Z"))
202
+ dropped = spool.purge_stale(cutoff_iso)
203
+ if dropped:
204
+ logger.info("activity.stale_discarded rows=%d older_than_s=%.0f "
205
+ "(live tracking: only recent activity is shipped)", dropped, max_age)
206
+ return dropped
207
+
208
+
191
209
  async def ship_tick(
192
210
  is_vps: Callable[[], Awaitable[bool | None]] | None = None,
193
211
  ) -> FlushResult | None:
194
- """One cron tick: ship only from the caller's assigned VPS.
212
+ """One cron tick: ship only from the caller's assigned VPS, newest first.
195
213
 
196
214
  The relay files every batch under the TOKEN's VPS, so a laptop running the SDK
197
215
  with the same token put its own events in the VPS's audit trail. On a machine
198
216
  that is definitely not the VPS the queue is discarded — kept, it would only grow
199
217
  forever, since that machine will never be allowed to send it. When the answer
200
- is unknown (profile unreachable) everything is kept for the next tick.
218
+ is unknown (profile unreachable) everything recent is kept for the next tick.
219
+
220
+ Every tick first discards events older than ``AGENTOS_ACTIVITY_MAX_AGE_S``
221
+ (default one hour): the trail is for live tracking, and a backlog — a host
222
+ that was offline, or flooded by a brute-force attack — must never hold the
223
+ current activity back.
201
224
  """
225
+ try:
226
+ purge_stale_rows()
227
+ except Exception as exc: # noqa: BLE001 — a purge failure must not stop shipping
228
+ logger.warning("activity: stale purge failed: %s", exc)
202
229
  if is_vps is None:
203
230
  from sdk import vps_identity
204
231
  is_vps = vps_identity.is_assigned_vps
@@ -222,7 +249,7 @@ async def ship_tick(
222
249
 
223
250
 
224
251
  async def run_forever(interval_s: float | None = None, *, initial_delay_s: float = 30.0) -> None:
225
- """Flush every ``interval_s`` (default 5 minutes) until cancelled."""
252
+ """Flush every ``interval_s`` (default 60 s) until cancelled."""
226
253
  await asyncio.sleep(initial_delay_s)
227
254
  while True:
228
255
  try:
@@ -169,33 +169,68 @@ class Spool:
169
169
  max_sessions: int = MAX_SESSIONS_PER_CHUNK,
170
170
  max_bytes: int = MAX_CHUNK_BYTES,
171
171
  ) -> Chunk:
172
- """The oldest rows that fit the per-request limits, in arrival order.
173
-
174
- Arrival order matters: a session is queued before its first event, so
175
- taking rows by ``seq`` ships a session no later than the events that
176
- reference it. Does NOT remove anything — call :meth:`ack` after delivery.
172
+ """Queued sessions, then the NEWEST events, within the per-request limits.
173
+
174
+ Live tracking needs the latest activity first. Shipping oldest-first left
175
+ busy hosts (a brute-forced RDP port alone is thousands of events an hour)
176
+ days behind, so the dashboard showed Tuesday's events on Thursday. Sessions
177
+ still go first, in arrival order: they are few, and an event must never
178
+ reach the relay ahead of the session it references. Does NOT remove
179
+ anything — call :meth:`ack` after delivery.
177
180
  """
178
181
  chunk = Chunk()
179
- n_events = n_sessions = size = 0
182
+ size = 0
180
183
  with self._lock:
181
- rows = self._db.execute(
182
- "SELECT seq, rev, kind, payload FROM spool ORDER BY seq LIMIT ?",
183
- (max_events + max_sessions,),
184
+ sessions = self._db.execute(
185
+ "SELECT seq, rev, kind, payload FROM spool WHERE kind = 'session' "
186
+ "ORDER BY seq LIMIT ?",
187
+ (max_sessions,),
184
188
  ).fetchall()
185
- for seq, rev, kind, payload in rows:
186
- if kind == "event" and n_events >= max_events:
187
- break # keep strict order: stop rather than skip ahead
188
- if kind == "session" and n_sessions >= max_sessions:
189
- break
189
+ events = self._db.execute(
190
+ "SELECT seq, rev, kind, payload FROM spool WHERE kind = 'event' "
191
+ "ORDER BY seq DESC LIMIT ?",
192
+ (max_events,),
193
+ ).fetchall()
194
+ for seq, rev, kind, payload in [*sessions, *events]:
190
195
  if chunk.rows and size + len(payload) > max_bytes:
191
196
  break
192
197
  # Re-fit on the way out too: rows queued by an older SDK predate the clamp.
193
198
  chunk.rows.append(Row(seq, rev, kind, fit_wire_limits(kind, json.loads(payload))))
194
- n_events += kind == "event"
195
- n_sessions += kind == "session"
196
199
  size += len(payload)
197
200
  return chunk
198
201
 
202
+ def purge_stale(self, cutoff_iso: str) -> int:
203
+ """Delete events that happened before ``cutoff_iso``, and sessions that ENDED
204
+ before it. Returns the rows removed.
205
+
206
+ For live tracking an event an hour old is history, and shipping it only
207
+ delays the current ones. Open sessions are kept whatever their age — a
208
+ login that is still going on is live state. ``cutoff_iso`` uses the wire
209
+ format (``2026-10-22T07:06:01.971Z``), so string order is time order.
210
+ """
211
+ with self._lock:
212
+ try:
213
+ cur = self._db.execute(
214
+ "DELETE FROM spool WHERE "
215
+ "(kind = 'event' AND json_extract(payload, '$.occurredAt') < ?) OR "
216
+ "(kind = 'session' AND json_extract(payload, '$.endedAt') IS NOT NULL "
217
+ " AND json_extract(payload, '$.endedAt') < ?)",
218
+ (cutoff_iso, cutoff_iso),
219
+ )
220
+ return int(cur.rowcount or 0)
221
+ except sqlite3.OperationalError:
222
+ # SQLite built without JSON1: filter in Python instead.
223
+ stale = []
224
+ for seq, kind, payload in self._db.execute(
225
+ "SELECT seq, kind, payload FROM spool"
226
+ ).fetchall():
227
+ wire = json.loads(payload)
228
+ when = wire.get("occurredAt") if kind == "event" else wire.get("endedAt")
229
+ if when and str(when) < cutoff_iso:
230
+ stale.append((seq,))
231
+ self._db.executemany("DELETE FROM spool WHERE seq = ?", stale)
232
+ return len(stale)
233
+
199
234
  # ── ack ──────────────────────────────────────────────────────────────────
200
235
 
201
236
  def ack(self, chunk: Chunk) -> None:
@@ -31,8 +31,15 @@ def _isolated(tmp_path, monkeypatch):
31
31
 
32
32
 
33
33
  def _queued():
34
+ """Queued sessions and events in the order the collector RECORDED them.
35
+
36
+ The shipper sends newest-first (live tracking); these tests are about what a
37
+ collector recorded and in which order, so read the rows back by ``seq``.
38
+ """
34
39
  chunk = settings.get_spool().next_chunk()
35
- return chunk.sessions, chunk.events
40
+ rows = sorted(chunk.rows, key=lambda r: r.seq)
41
+ return ([r.wire for r in rows if r.kind == "session"],
42
+ [r.wire for r in rows if r.kind == "event"])
36
43
 
37
44
 
38
45
  # ─────────────────────────────── Linux / auditd ───────────────────────────────
@@ -0,0 +1,145 @@
1
+ """Live tracking: the newest activity ships first, and stale backlog is discarded.
2
+
3
+ The incident: hosts that had been offline, or flooded by a brute-forced RDP port,
4
+ shipped their spool oldest-first. At 10:00 several VPS were delivering events from
5
+ two and three days earlier, so the dashboard was never live.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import time
11
+ from datetime import datetime, timezone
12
+
13
+ import pytest
14
+
15
+ from sdk.activity import settings, shipper
16
+ from sdk.activity.spool import Spool
17
+
18
+ NOW = datetime(2026, 10, 22, 10, 0, 0, tzinfo=timezone.utc).timestamp()
19
+
20
+
21
+ def _iso(minutes_ago: float) -> str:
22
+ t = datetime.fromtimestamp(NOW - minutes_ago * 60, timezone.utc)
23
+ return t.isoformat(timespec="milliseconds").replace("+00:00", "Z")
24
+
25
+
26
+ def _ev(eid: str, minutes_ago: float) -> dict:
27
+ return {"eventId": eid, "occurredAt": _iso(minutes_ago), "os": "LINUX", "source": "AUDITD",
28
+ "category": "COMMAND", "action": "exec", "actorType": "HUMAN", "actorConfidence": 90}
29
+
30
+
31
+ def _sess(key: str, started_ago: float, ended_ago: float | None = None) -> dict:
32
+ s = {"externalKey": key, "kind": "SSH", "actorType": "HUMAN", "actorConfidence": 90,
33
+ "startedAt": _iso(started_ago)}
34
+ if ended_ago is not None:
35
+ s["endedAt"] = _iso(ended_ago)
36
+ return s
37
+
38
+
39
+ @pytest.fixture
40
+ def spool(tmp_path):
41
+ s = Spool(tmp_path / "live.db")
42
+ yield s
43
+ s.close()
44
+
45
+
46
+ @pytest.fixture(autouse=True)
47
+ def hermetic(monkeypatch):
48
+ import sdk.config as cfg
49
+ monkeypatch.setattr(cfg, "_dotenv_value", lambda key: "")
50
+ monkeypatch.delenv("AGENTOS_ACTIVITY_MAX_AGE_S", raising=False)
51
+
52
+
53
+ class TestStaleBacklogIsDiscarded:
54
+ def test_events_older_than_an_hour_are_dropped(self, spool):
55
+ spool.add_event(_ev("days-old", 3 * 24 * 60))
56
+ spool.add_event(_ev("61-min", 61))
57
+ spool.add_event(_ev("59-min", 59))
58
+ spool.add_event(_ev("now", 0))
59
+ assert shipper.purge_stale_rows(spool, now=NOW) == 2
60
+ assert sorted(e["eventId"] for e in spool.next_chunk().events) == ["59-min", "now"]
61
+
62
+ def test_an_open_session_is_kept_however_old(self, spool):
63
+ spool.add_session(_sess("still-logged-in", started_ago=600))
64
+ spool.add_session(_sess("ended-long-ago", started_ago=600, ended_ago=300))
65
+ spool.add_session(_sess("ended-just-now", started_ago=600, ended_ago=5))
66
+ assert shipper.purge_stale_rows(spool, now=NOW) == 1
67
+ assert sorted(s["externalKey"] for s in spool.next_chunk().sessions) == [
68
+ "ended-just-now", "still-logged-in"]
69
+
70
+ def test_the_cut_off_is_configurable(self, spool, monkeypatch):
71
+ monkeypatch.setenv("AGENTOS_ACTIVITY_MAX_AGE_S", "86400")
72
+ spool.add_event(_ev("3h", 180))
73
+ spool.add_event(_ev("2d", 2 * 24 * 60))
74
+ assert shipper.purge_stale_rows(spool, now=NOW) == 1
75
+ assert [e["eventId"] for e in spool.next_chunk().events] == ["3h"]
76
+
77
+ def test_zero_keeps_everything(self, spool, monkeypatch):
78
+ monkeypatch.setenv("AGENTOS_ACTIVITY_MAX_AGE_S", "0")
79
+ spool.add_event(_ev("ancient", 10 * 24 * 60))
80
+ assert shipper.purge_stale_rows(spool, now=NOW) == 0
81
+ assert spool.count() == 1
82
+
83
+ def test_python_fallback_matches_json1(self, spool, monkeypatch):
84
+ """SQLite without JSON1 filters in Python, to the same result."""
85
+ import sqlite3
86
+
87
+ spool.add_event(_ev("old", 120))
88
+ spool.add_event(_ev("new", 1))
89
+ real = spool._db
90
+
91
+ class NoJson:
92
+ def execute(self, sql, *a):
93
+ if "json_extract" in sql:
94
+ raise sqlite3.OperationalError("no such function: json_extract")
95
+ return real.execute(sql, *a)
96
+
97
+ def executemany(self, sql, *a):
98
+ return real.executemany(sql, *a)
99
+ monkeypatch.setattr(spool, "_db", NoJson())
100
+ assert shipper.purge_stale_rows(spool, now=NOW) == 1
101
+ monkeypatch.setattr(spool, "_db", real)
102
+ assert [e["eventId"] for e in spool.next_chunk().events] == ["new"]
103
+
104
+
105
+ class TestNewestFirst:
106
+ def test_a_backlog_never_holds_back_the_current_events(self, spool):
107
+ for i in range(1200): # a flood, oldest first
108
+ spool.add_event(_ev(f"old{i}", 50 - i * 0.01))
109
+ spool.add_event(_ev("current", 0))
110
+ first = spool.next_chunk(max_events=500)
111
+ assert first.events[0]["eventId"] == "current"
112
+
113
+
114
+ class TestShipTickPurgesFirst:
115
+ def test_stale_rows_are_gone_before_anything_ships(self, tmp_path, monkeypatch):
116
+ s = Spool(tmp_path / "tick.db")
117
+ s.add_event(_ev("stale", 120))
118
+ s.add_event(_ev("fresh", 1))
119
+ monkeypatch.setattr(settings, "get_spool", lambda: s)
120
+ monkeypatch.setattr(time, "time", lambda: NOW)
121
+ seen = []
122
+
123
+ async def fake_flush():
124
+ seen.extend(e["eventId"] for e in s.next_chunk().events)
125
+ return shipper.FlushResult()
126
+ monkeypatch.setattr(shipper, "flush_once", fake_flush)
127
+
128
+ async def yes():
129
+ return True
130
+ asyncio.run(shipper.ship_tick(yes))
131
+ s.close()
132
+ assert seen == ["fresh"]
133
+
134
+ def test_purge_also_runs_when_vps_identity_is_unknown(self, tmp_path, monkeypatch):
135
+ s = Spool(tmp_path / "tick2.db")
136
+ s.add_event(_ev("stale", 120))
137
+ monkeypatch.setattr(settings, "get_spool", lambda: s)
138
+ monkeypatch.setattr(time, "time", lambda: NOW)
139
+
140
+ async def unknown():
141
+ return None
142
+ asyncio.run(shipper.ship_tick(unknown))
143
+ n = s.count()
144
+ s.close()
145
+ assert n == 0
@@ -31,8 +31,15 @@ def _isolated(tmp_path, monkeypatch):
31
31
 
32
32
 
33
33
  def _queued():
34
+ """Queued sessions and events in the order the collector RECORDED them.
35
+
36
+ The shipper sends newest-first (live tracking); these tests are about what a
37
+ collector recorded and in which order, so read the rows back by ``seq``.
38
+ """
34
39
  chunk = settings.get_spool().next_chunk()
35
- return chunk.sessions, chunk.events
40
+ rows = sorted(chunk.rows, key=lambda r: r.seq)
41
+ return ([r.wire for r in rows if r.kind == "session"],
42
+ [r.wire for r in rows if r.kind == "event"])
36
43
 
37
44
 
38
45
  USERS = {501: "alice", 502: "agentos", 0: "root"}
@@ -83,10 +83,10 @@ class TestSpool:
83
83
 
84
84
  spool.add_event(_ev("E1", metadata={"blob": "x" * (MAX_DICT_FIELD_BYTES * 2)}))
85
85
  spool.add_event(_ev("E2", metadata={"small": 1}))
86
- events = spool.next_chunk().events
87
- assert events[0]["metadata"]["truncated"] is True
88
- assert events[0]["metadata"]["originalBytes"] > MAX_DICT_FIELD_BYTES
89
- assert events[1]["metadata"] == {"small": 1}
86
+ events = {e["eventId"]: e for e in spool.next_chunk().events}
87
+ assert events["E1"]["metadata"]["truncated"] is True
88
+ assert events["E1"]["metadata"]["originalBytes"] > MAX_DICT_FIELD_BYTES
89
+ assert events["E2"]["metadata"] == {"small": 1}
90
90
 
91
91
  def test_a_single_worst_case_row_fits_comfortably_in_one_request(self, spool):
92
92
  from sdk.activity.models import MAX_DICT_FIELD_BYTES
@@ -105,21 +105,23 @@ class TestSpool:
105
105
 
106
106
  assert 1_000_000 < spool_mod.MAX_CHUNK_BYTES < 10 * 1024 * 1024
107
107
 
108
- def test_rows_come_back_in_arrival_order_session_first(self, spool):
108
+ def test_sessions_first_then_the_newest_events(self, spool):
109
+ """Live tracking: the latest activity ships first. Sessions still lead, so an
110
+ event never reaches the relay ahead of the session it references."""
109
111
  spool.add_session(_sess("S1"))
110
112
  spool.add_event(_ev("E1"))
111
113
  spool.add_event(_ev("E2"))
112
114
  chunk = spool.next_chunk()
113
115
  assert [r.kind for r in chunk.rows] == ["session", "event", "event"]
114
- assert [e["eventId"] for e in chunk.events] == ["E1", "E2"]
116
+ assert [e["eventId"] for e in chunk.events] == ["E2", "E1"]
115
117
 
116
118
  def test_next_chunk_respects_the_event_limit_and_leaves_the_rest(self, spool):
117
119
  for i in range(5):
118
120
  spool.add_event(_ev(f"E{i}"))
119
121
  chunk = spool.next_chunk(max_events=2)
120
- assert [e["eventId"] for e in chunk.events] == ["E0", "E1"]
122
+ assert [e["eventId"] for e in chunk.events] == ["E4", "E3"]
121
123
  spool.ack(chunk)
122
- assert [e["eventId"] for e in spool.next_chunk(max_events=10).events] == ["E2", "E3", "E4"]
124
+ assert [e["eventId"] for e in spool.next_chunk(max_events=10).events] == ["E2", "E1", "E0"]
123
125
 
124
126
  def test_next_chunk_respects_the_byte_limit(self, spool):
125
127
  for i in range(10):
@@ -169,7 +171,7 @@ class TestSpool:
169
171
  for i in range(5):
170
172
  s.add_event(_ev(f"E{i}"))
171
173
  assert s.count() == 3
172
- assert [e["eventId"] for e in s.next_chunk().events] == ["E2", "E3", "E4"]
174
+ assert [e["eventId"] for e in s.next_chunk().events] == ["E4", "E3", "E2"]
173
175
  finally:
174
176
  s.close()
175
177
 
@@ -179,7 +181,7 @@ class TestSpool:
179
181
  spool.add_event(_ev(f"E{i}"))
180
182
  left, right = spool.next_chunk().split()
181
183
  assert [r.kind for r in left.rows] == ["session", "event"]
182
- assert [e["eventId"] for e in right.events] == ["E1", "E2", "E3"]
184
+ assert [e["eventId"] for e in right.events] == ["E2", "E1", "E0"]
183
185
 
184
186
 
185
187
  class FakeSender:
@@ -355,7 +357,8 @@ class TestSettings:
355
357
  def test_defaults(self):
356
358
  assert settings.enabled() is True
357
359
  assert settings.audit_enabled() is False
358
- assert settings.ship_interval_seconds() == 300.0
360
+ assert settings.ship_interval_seconds() == 60.0
361
+ assert settings.max_age_seconds() == 3600.0
359
362
 
360
363
  def test_can_be_disabled(self, monkeypatch):
361
364
  monkeypatch.setenv("AGENTOS_ACTIVITY", "off")
@@ -373,7 +376,7 @@ class TestSettings:
373
376
 
374
377
  def test_garbage_interval_falls_back_to_default(self, monkeypatch):
375
378
  monkeypatch.setenv("AGENTOS_ACTIVITY_SHIP_INTERVAL_S", "often")
376
- assert settings.ship_interval_seconds() == 300.0
379
+ assert settings.ship_interval_seconds() == settings.DEFAULT_SHIP_INTERVAL_S
377
380
 
378
381
  def test_start_is_a_noop_when_disabled(self, monkeypatch):
379
382
  monkeypatch.setenv("AGENTOS_ACTIVITY", "0")
@@ -438,8 +441,9 @@ class TestRecorder:
438
441
  recorder.agent_run_finished(RUN, ok=False, detail="boom")
439
442
  sessions, events = _queued()
440
443
  assert len(sessions) == 1 and "endedAt" in sessions[0]
441
- assert [e["action"] for e in events] == ["run.start", "run.end"]
442
- assert events[1]["exitCode"] == 1 and events[1]["severity"] == "WARNING"
444
+ by_action = {e["action"]: e for e in events} # shipped newest-first
445
+ assert sorted(by_action) == ["run.end", "run.start"]
446
+ assert by_action["run.end"]["exitCode"] == 1 and by_action["run.end"]["severity"] == "WARNING"
443
447
 
444
448
  def test_a_non_uuid_agent_id_is_omitted_not_sent(self):
445
449
  recorder.agent_run_started(RUN, agent_id="nightly-report")