devops-bot-sdk 1.6.66__tar.gz → 1.6.69__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 (233) hide show
  1. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/SOURCES.txt +4 -0
  4. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/chat.py +51 -1
  6. devops_bot_sdk-1.6.69/sdk/schedule_tickets.py +201 -0
  7. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/scheduler.py +119 -15
  8. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/service.py +94 -22
  9. devops_bot_sdk-1.6.69/tests/test_schedule_tickets.py +218 -0
  10. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_scheduler.py +36 -0
  11. devops_bot_sdk-1.6.69/tests/test_scheduler_transient_retry.py +230 -0
  12. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_update_timer.py +2 -0
  13. devops_bot_sdk-1.6.69/tests/test_windows_task_paths.py +172 -0
  14. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/README.md +0 -0
  15. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  16. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  17. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/requires.txt +0 -0
  18. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  19. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/pyproject.toml +0 -0
  20. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/__init__.py +0 -0
  21. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/classifier.py +0 -0
  22. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/__init__.py +0 -0
  23. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/common.py +0 -0
  24. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/linux_auditd.py +0 -0
  25. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/macos_eslogger.py +0 -0
  26. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/windows_eventlog.py +0 -0
  27. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/models.py +0 -0
  28. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/os_setup.py +0 -0
  29. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/recorder.py +0 -0
  30. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/redact.py +0 -0
  31. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/settings.py +0 -0
  32. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/shipper.py +0 -0
  33. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/spool.py +0 -0
  34. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agentd.py +0 -0
  35. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/__init__.py +0 -0
  36. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/activity.py +0 -0
  37. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/builder.py +0 -0
  38. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/connectors.py +0 -0
  39. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/__init__.py +0 -0
  40. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/claude_code.py +0 -0
  41. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/http.py +0 -0
  42. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/mcp.py +0 -0
  43. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/n8n.py +0 -0
  44. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/shell.py +0 -0
  45. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/integrations.py +0 -0
  46. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/inventory.py +0 -0
  47. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/library/__init__.py +0 -0
  48. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/manifest.py +0 -0
  49. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/prompt_studio.py +0 -0
  50. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/prompt_template.py +0 -0
  51. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/prompt_vars.py +0 -0
  52. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/run_log.py +0 -0
  53. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/schedules.py +0 -0
  54. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/secrets.py +0 -0
  55. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/ticket_run.py +0 -0
  56. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/usage.py +0 -0
  57. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/varstore.py +0 -0
  58. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/browser_profile.py +0 -0
  59. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/cli.py +0 -0
  60. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/client.py +0 -0
  61. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/collectors/__init__.py +0 -0
  62. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/collectors/files.py +0 -0
  63. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/collectors/process.py +0 -0
  64. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/collectors/screenshot.py +0 -0
  65. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/config.py +0 -0
  66. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/crucial.py +0 -0
  67. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/custom_agents.py +0 -0
  68. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/departments.py +0 -0
  69. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/__init__.py +0 -0
  70. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/assets.py +0 -0
  71. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/bootstrap.py +0 -0
  72. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/browser.py +0 -0
  73. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/compare.py +0 -0
  74. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/loop.py +0 -0
  75. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/estimator.py +0 -0
  76. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/exceptions.py +0 -0
  77. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/firewall.py +0 -0
  78. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/formats.py +0 -0
  79. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/git_ops.py +0 -0
  80. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/github_fetch.py +0 -0
  81. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/github_ingest.py +0 -0
  82. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/graph_recon.py +0 -0
  83. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/graphify.py +0 -0
  84. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/hooks/__init__.py +0 -0
  85. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/hooks/crucial_guard.py +0 -0
  86. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/intake.py +0 -0
  87. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ipc/__init__.py +0 -0
  88. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ipc/electron_bridge.py +0 -0
  89. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ipc/handlers.py +0 -0
  90. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/journal.py +0 -0
  91. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/local_exec.py +0 -0
  92. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/memory_guard.py +0 -0
  93. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/__init__.py +0 -0
  94. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/envelope.py +0 -0
  95. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/requests.py +0 -0
  96. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/responses.py +0 -0
  97. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/snapshots.py +0 -0
  98. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/phase_timer.py +0 -0
  99. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/plan_run.py +0 -0
  100. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/platform_compat.py +0 -0
  101. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/prd_progress.py +0 -0
  102. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/proof_readers.py +0 -0
  103. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/providers.py +0 -0
  104. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/py.typed +0 -0
  105. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/resume.py +0 -0
  106. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/router.py +0 -0
  107. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/run_auto.py +0 -0
  108. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/self_report.py +0 -0
  109. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/self_report_local.py +0 -0
  110. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/sse.py +0 -0
  111. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/swagger.py +0 -0
  112. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/task_file.py +0 -0
  113. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/task_types.py +0 -0
  114. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/test.py +0 -0
  115. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/test_pipeline.py +0 -0
  116. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ticket_fields.py +0 -0
  117. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/tickets.py +0 -0
  118. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/tiers/__init__.py +0 -0
  119. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/tiers/mini_agent.py +0 -0
  120. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/__init__.py +0 -0
  121. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/api.py +0 -0
  122. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/dashboard.py +0 -0
  123. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/health.py +0 -0
  124. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/ip_allowlist.py +0 -0
  125. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/jobs.py +0 -0
  126. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/models.py +0 -0
  127. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/runner.py +0 -0
  128. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/security.py +0 -0
  129. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/server.py +0 -0
  130. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/session.py +0 -0
  131. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/setup_manager.py +0 -0
  132. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/agents.html +0 -0
  133. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/app.js +0 -0
  134. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/chat.html +0 -0
  135. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/custom-agents.html +0 -0
  136. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/default-agents.html +0 -0
  137. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/index.html +0 -0
  138. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/report.html +0 -0
  139. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/runs.html +0 -0
  140. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/schedules.html +0 -0
  141. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/setup.html +0 -0
  142. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/styles.css +0 -0
  143. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/tickets.html +0 -0
  144. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/swagger.py +0 -0
  145. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/updater.py +0 -0
  146. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/url_access.py +0 -0
  147. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  148. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/vps_health.py +0 -0
  149. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/write_hook.py +0 -0
  150. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/setup.cfg +0 -0
  151. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_collectors.py +0 -0
  152. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_core.py +0 -0
  153. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_macos.py +0 -0
  154. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_os_setup.py +0 -0
  155. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_pipeline.py +0 -0
  156. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_activity.py +0 -0
  157. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_activity_tracking.py +0 -0
  158. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_chat_features.py +0 -0
  159. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_crud_cli.py +0 -0
  160. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_listing.py +0 -0
  161. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_prompt_async.py +0 -0
  162. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_prompt_models.py +0 -0
  163. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_run_ticket.py +0 -0
  164. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_schedules.py +0 -0
  165. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_workspace_settings.py +0 -0
  166. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agents_connectors_builder.py +0 -0
  167. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_allowed_tools_config.py +0 -0
  168. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_auto_agent_dispatch.py +0 -0
  169. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_browser_profile.py +0 -0
  170. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_cheap_tier.py +0 -0
  171. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_client_jira_projects.py +0 -0
  172. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_config_token_diagnostics.py +0 -0
  173. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_configure_token_owner.py +0 -0
  174. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_console_encoding.py +0 -0
  175. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_crash_recovery.py +0 -0
  176. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_departments.py +0 -0
  177. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_done_transition.py +0 -0
  178. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_epic_decompose_run.py +0 -0
  179. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_epic_inherited_agent_id.py +0 -0
  180. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_epic_session_isolation.py +0 -0
  181. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_estimator.py +0 -0
  182. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_fallback_cli.py +0 -0
  183. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_firewall_open_on_update.py +0 -0
  184. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_formats.py +0 -0
  185. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_git_action.py +0 -0
  186. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_github_fetch.py +0 -0
  187. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_github_ingest.py +0 -0
  188. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_graph_recon.py +0 -0
  189. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_graphify_build.py +0 -0
  190. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_graphify_hook_install.py +0 -0
  191. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_health.py +0 -0
  192. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_intake_job_outcome.py +0 -0
  193. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_intake_plan_review.py +0 -0
  194. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_journal.py +0 -0
  195. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_local_exec_provider.py +0 -0
  196. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_mcp_route_contract.py +0 -0
  197. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_memory_guard.py +0 -0
  198. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_phase_timer.py +0 -0
  199. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_plan_new_command.py +0 -0
  200. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_plan_run_activity.py +0 -0
  201. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_pr_identity.py +0 -0
  202. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_prd_artifact.py +0 -0
  203. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_project_path_resolution.py +0 -0
  204. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_proof_readers.py +0 -0
  205. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_providers.py +0 -0
  206. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_recon_session_isolation.py +0 -0
  207. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_resume.py +0 -0
  208. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_router.py +0 -0
  209. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_run_auto_agent_only.py +0 -0
  210. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_run_outputs.py +0 -0
  211. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_run_verdict_is_honest.py +0 -0
  212. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_schedule_api.py +0 -0
  213. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_schedule_outputs_and_plans.py +0 -0
  214. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_self_report.py +0 -0
  215. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_service_token_warning.py +0 -0
  216. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_stale_panel_restart.py +0 -0
  217. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_swagger_command.py +0 -0
  218. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_task_file_and_plan_run.py +0 -0
  219. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_task_file_format_doc.py +0 -0
  220. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ticket_cli.py +0 -0
  221. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ticket_fields.py +0 -0
  222. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ticket_run_resolve_by_id.py +0 -0
  223. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ticket_speed_fixes.py +0 -0
  224. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_tier_flag_parity.py +0 -0
  225. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ui_gate.py +0 -0
  226. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ui_lifespan.py +0 -0
  227. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_updater_extras.py +0 -0
  228. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_updater_pep668.py +0 -0
  229. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_updater_reports_the_truth.py +0 -0
  230. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_url_access.py +0 -0
  231. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_vps_health.py +0 -0
  232. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_windows_loop_identity.py +0 -0
  233. {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/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.66
3
+ Version: 1.6.69
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.66
3
+ Version: 1.6.69
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
@@ -38,6 +38,7 @@ sdk/py.typed
38
38
  sdk/resume.py
39
39
  sdk/router.py
40
40
  sdk/run_auto.py
41
+ sdk/schedule_tickets.py
41
42
  sdk/scheduler.py
42
43
  sdk/self_report.py
43
44
  sdk/self_report_local.py
@@ -203,7 +204,9 @@ tests/test_run_outputs.py
203
204
  tests/test_run_verdict_is_honest.py
204
205
  tests/test_schedule_api.py
205
206
  tests/test_schedule_outputs_and_plans.py
207
+ tests/test_schedule_tickets.py
206
208
  tests/test_scheduler.py
209
+ tests/test_scheduler_transient_retry.py
207
210
  tests/test_self_report.py
208
211
  tests/test_service_token_warning.py
209
212
  tests/test_stale_panel_restart.py
@@ -224,4 +227,5 @@ tests/test_updater_reports_the_truth.py
224
227
  tests/test_url_access.py
225
228
  tests/test_vps_health.py
226
229
  tests/test_windows_loop_identity.py
230
+ tests/test_windows_task_paths.py
227
231
  tests/test_write_hook.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.66
3
+ Version: 1.6.69
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.66" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.69" # 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
@@ -41,6 +41,52 @@ def _tee_events(log_path, on_event: "EventHandler | None") -> "EventHandler":
41
41
  EventHandler = Callable[[dict], Awaitable[None]]
42
42
 
43
43
 
44
+ #: Pauses between attempts when an agent list cannot be read. The AI backend
45
+ #: verifies the co_ token against the ML gateway on every call and that check
46
+ #: flakes (a spurious 401 or a dropped connection for a few seconds), which made a
47
+ #: scheduled run give up on the first miss. Short, because a human may be waiting.
48
+ _LOOKUP_RETRY_DELAYS_S: tuple[float, ...] = (2.0, 5.0)
49
+
50
+
51
+ async def _fetch_rows(fetch) -> list[dict]:
52
+ """``fetch()`` with brief retries; raises the last error when every attempt fails."""
53
+ import asyncio
54
+
55
+ last: Exception | None = None
56
+ for delay in (0.0, *_LOOKUP_RETRY_DELAYS_S):
57
+ if delay:
58
+ await asyncio.sleep(delay)
59
+ try:
60
+ return await fetch() or []
61
+ except Exception as exc: # noqa: BLE001 — classified by the caller
62
+ last = exc
63
+ assert last is not None
64
+ raise last
65
+
66
+
67
+ async def _gateway_manifest(client, name: str):
68
+ """The agent from the ML gateway's listing, or ``None``.
69
+
70
+ The gateway reads the same ``agents`` table the AI backend re-projects, with no
71
+ second token-verification hop, so it stays readable when the backend flakes.
72
+ Consulted only after a backend source could not be read.
73
+ """
74
+ from sdk.agents.manifest import manifest_from_backend_row
75
+
76
+ fetch = getattr(client, "list_gateway_agents_by_scope", None)
77
+ if fetch is None:
78
+ return None
79
+ try:
80
+ scopes = await fetch() or {}
81
+ except Exception: # noqa: BLE001 — the fallback failing changes nothing
82
+ return None
83
+ for scope in ("personal", "department"):
84
+ for row in scopes.get(scope) or []:
85
+ if isinstance(row, dict) and row.get("name") == name:
86
+ return manifest_from_backend_row(row)
87
+ return None
88
+
89
+
44
90
  class AgentLookupUnavailable(LookupError):
45
91
  """The agent could not be found, but at least one source could not be READ.
46
92
 
@@ -88,7 +134,7 @@ async def resolve_agent(name: str, client=None) -> AgentManifest:
88
134
  if fetch is None:
89
135
  continue
90
136
  try:
91
- rows = await fetch() or []
137
+ rows = await _fetch_rows(fetch)
92
138
  except Exception as exc: # noqa: BLE001 — a fetch failure is not "not found"
93
139
  unreadable = True
94
140
  reasons.append(f"{label} could not be fetched ({str(exc) or type(exc).__name__})")
@@ -107,6 +153,10 @@ async def resolve_agent(name: str, client=None) -> AgentManifest:
107
153
  "not in ~/.agentos/agents/, your backend agents, or your department defaults"
108
154
  )
109
155
  if unreadable:
156
+ manifest = await _gateway_manifest(client, name)
157
+ if manifest is not None:
158
+ logger.info("agent lookup fell back to the ML gateway listing for %r", name)
159
+ return manifest
110
160
  # Do NOT say "no agent named …" here: a source went unread, so absence was
111
161
  # never established. Claiming it sends the user hunting for a typo when the
112
162
  # fix is a token or a network.
@@ -0,0 +1,201 @@
1
+ """A Jira ticket per scheduled run: open it, mark it In Progress, close it on proof.
2
+
3
+ Opt-in per schedule with a ``ticket`` block on the schedule record::
4
+
5
+ "ticket": {"project_key": "DBNF", "type": "Task", "priority": "Low"}
6
+
7
+ Lifecycle, driven by ``sdk.scheduler.fire``:
8
+
9
+ 1. ``open_ticket`` — POST /tasks-ml, then PUT status "In Progress".
10
+ 2. the agent runs exactly as it would without a ticket.
11
+ 3. ``close_ticket`` — "Done" only when the run succeeded AND its output file
12
+ exists and was written during this run (``evidence_path``); otherwise
13
+ "backlog" with the reason appended to the description.
14
+
15
+ Uses the ML gateway's /tasks-ml directly (``create_task_ml`` / ``update_task_ml``),
16
+ the same calls the Epic decomposition already relies on, so it works without any
17
+ backend deploy. Every function here is best-effort and never raises: a ticket is
18
+ bookkeeping, and losing a nightly run because Jira was slow would be the wrong
19
+ trade. The lowercase "backlog" is deliberate — the gateway validates against
20
+ ``{To Do, In Progress, Done, backlog}`` and 400s on "Backlog".
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from dataclasses import dataclass
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+
29
+ logger = logging.getLogger("sdk.schedule_tickets")
30
+
31
+ DONE = "Done"
32
+ IN_PROGRESS = "In Progress"
33
+ BACKLOG = "backlog"
34
+ _MAX_REASON = 500
35
+
36
+
37
+ @dataclass
38
+ class RunTicket:
39
+ task_id: str
40
+ key: str
41
+ description: str
42
+
43
+
44
+ def to_record(ticket: RunTicket | None) -> dict | None:
45
+ """Plain dict to persist on the schedule while a run is being retried."""
46
+ if ticket is None:
47
+ return None
48
+ return {"task_id": ticket.task_id, "key": ticket.key, "description": ticket.description}
49
+
50
+
51
+ def from_record(record) -> RunTicket | None:
52
+ """The ticket a previous attempt of this run opened, if one was persisted."""
53
+ if not isinstance(record, dict) or not record.get("task_id"):
54
+ return None
55
+ return RunTicket(task_id=str(record["task_id"]), key=str(record.get("key") or ""),
56
+ description=str(record.get("description") or ""))
57
+
58
+
59
+ def enabled(schedule: dict) -> bool:
60
+ cfg = schedule.get("ticket")
61
+ return isinstance(cfg, dict) and bool(str(cfg.get("project_key") or "").strip())
62
+
63
+
64
+ def _summary(schedule: dict, when: datetime) -> str:
65
+ variables = schedule.get("variables") or {}
66
+ target = variables.get("repo") or ""
67
+ branch = variables.get("branch") or ""
68
+ if target and branch:
69
+ target = f"{target}@{branch}"
70
+ parts = ["[Scheduled]", str(schedule.get("agent") or "agent")]
71
+ if target:
72
+ parts.append(f"— {target}")
73
+ parts.append(f"— {when:%Y-%m-%d}")
74
+ return " ".join(parts)[:255]
75
+
76
+
77
+ def _description(schedule: dict, expected: str) -> str:
78
+ variables = schedule.get("variables") or {}
79
+ lines = [
80
+ f"Automated run of agent '{schedule.get('agent')}' by the AgentOS scheduler.",
81
+ f"Schedule: {schedule.get('cron') or schedule.get('run_at')} ({schedule.get('timezone')}), "
82
+ f"id {schedule.get('schedule_id')}.",
83
+ ]
84
+ if expected:
85
+ lines.append(f"Output: {expected}")
86
+ shown = {k: v for k, v in variables.items() if k in ("repo", "repo_path", "branch", "output_file")}
87
+ if shown:
88
+ lines.append("Inputs: " + ", ".join(f"{k}={v}" for k, v in shown.items()))
89
+ lines.append("Closed as Done only when the run succeeds and its output file is written.")
90
+ return "\n".join(lines)
91
+
92
+
93
+ def expected_output(schedule: dict) -> str:
94
+ """The file (or folder) this run is expected to write, for the ticket text."""
95
+ variables = schedule.get("variables") or {}
96
+ return str(variables.get("output_file") or schedule.get("output_dir") or "")
97
+
98
+
99
+ def evidence_path(schedule: dict, record: Path | None, delivered_to: str,
100
+ started_at: datetime) -> tuple[Path | None, str]:
101
+ """``(path, "")`` when the run's output exists and is fresh, else ``(None, why)``.
102
+
103
+ The agent's own ``output_file`` variable is the strongest proof (the per-repo
104
+ suggestion and quality files). Failing that, the run record copied into the
105
+ schedule's export folder (the AI Research Agent's daily file). Failing that,
106
+ the run record itself.
107
+ """
108
+ variables = schedule.get("variables") or {}
109
+ want = str(variables.get("output_file") or "").strip()
110
+ if want:
111
+ candidate = Path(want).expanduser()
112
+ elif delivered_to and record is not None:
113
+ candidate = Path(delivered_to) / Path(record).name
114
+ elif record is not None:
115
+ candidate = Path(record)
116
+ else:
117
+ return None, "the run produced no output file"
118
+ try:
119
+ stat = candidate.stat()
120
+ except OSError:
121
+ return None, f"output file was not created: {candidate}"
122
+ written = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
123
+ if written < started_at.replace(microsecond=0):
124
+ return None, f"output file was not updated by this run: {candidate}"
125
+ return candidate, ""
126
+
127
+
128
+ async def _owner_email(client) -> str:
129
+ try:
130
+ return str((await client.profile()).email or "").strip()
131
+ except Exception: # noqa: BLE001
132
+ return ""
133
+
134
+
135
+ async def open_ticket(client, schedule: dict, when: datetime | None = None) -> RunTicket | None:
136
+ """Create the run's ticket and move it to In Progress. ``None`` on any failure."""
137
+ if not enabled(schedule):
138
+ return None
139
+ cfg = schedule["ticket"]
140
+ when = when or datetime.now(timezone.utc)
141
+ description = _description(schedule, expected_output(schedule))
142
+ fields = {
143
+ "projectKey": str(cfg["project_key"]).strip(),
144
+ "type": str(cfg.get("type") or "Task"),
145
+ "priority": str(cfg.get("priority") or "Low"),
146
+ "summary": _summary(schedule, when),
147
+ "description": description,
148
+ }
149
+ email = await _owner_email(client)
150
+ if email:
151
+ fields["assigneeEmail"] = email
152
+ try:
153
+ task_id = await client.create_task_ml(fields)
154
+ except Exception as exc: # noqa: BLE001
155
+ logger.warning("schedule_tickets.create_failed sid=%s err=%s", schedule.get("schedule_id"), exc)
156
+ return None
157
+ if not task_id:
158
+ logger.warning("schedule_tickets.create_failed sid=%s detail=%s",
159
+ schedule.get("schedule_id"), getattr(client, "last_task_ml_error", ""))
160
+ return None
161
+ key = ""
162
+ try:
163
+ record = await client.get_task_ml(task_id) or {}
164
+ key = str(record.get("jiraIssueKey") or record.get("jiraKey") or "")
165
+ except Exception: # noqa: BLE001 — the key is for display only
166
+ pass
167
+ ticket = RunTicket(task_id=str(task_id), key=key, description=description)
168
+ await _set_status(client, ticket, IN_PROGRESS)
169
+ return ticket
170
+
171
+
172
+ async def _set_status(client, ticket: RunTicket, status: str, extra: dict | None = None) -> bool:
173
+ try:
174
+ ok = await client.update_task_ml(ticket.task_id, {"status": status, **(extra or {})})
175
+ except Exception as exc: # noqa: BLE001
176
+ logger.warning("schedule_tickets.status_failed task=%s status=%s err=%s",
177
+ ticket.task_id, status, exc)
178
+ return False
179
+ if not ok:
180
+ logger.warning("schedule_tickets.status_failed task=%s status=%s detail=%s",
181
+ ticket.task_id, status, getattr(client, "last_task_ml_error", ""))
182
+ return bool(ok)
183
+
184
+
185
+ async def close_ticket(client, ticket: RunTicket | None, *, ok: bool, reason: str = "",
186
+ output: Path | None = None) -> str:
187
+ """Done on proven success, backlog otherwise. Returns the status set ("" if none)."""
188
+ if ticket is None:
189
+ return ""
190
+ if ok and output is not None:
191
+ done = await _set_status(client, ticket, DONE,
192
+ {"description": f"{ticket.description}\nResult: {output}"})
193
+ return DONE if done else ""
194
+ why = (reason or "the run did not complete").strip()[:_MAX_REASON]
195
+ moved = await _set_status(client, ticket, BACKLOG,
196
+ {"description": f"{ticket.description}\nFailed: {why}"})
197
+ return BACKLOG if moved else ""
198
+
199
+
200
+ def label(ticket: RunTicket | None) -> str:
201
+ return (ticket.key or ticket.task_id) if ticket else ""
@@ -56,6 +56,17 @@ _MAX_JITTER_S = 60.0
56
56
 
57
57
  _semaphore: asyncio.Semaphore | None = None
58
58
  _running: set[str] = set() # schedule_ids currently in flight, this process
59
+ # schedule_ids dispatched and not yet finished — including ones still WAITING for
60
+ # the semaphore. mark_fired only runs once fire() starts, so without this a due
61
+ # schedule queued behind a long run was re-dispatched on every tick and then ran
62
+ # once per queued copy.
63
+ _queued: set[str] = set()
64
+
65
+ # A run whose agent could not be LOOKED UP (backend unreadable) is re-armed and
66
+ # retried instead of being lost for the day: this many times, this far apart.
67
+ # 3 x 5 min stays well inside the default 1 h catch-up grace window.
68
+ MAX_TRANSIENT_RETRIES = 3
69
+ TRANSIENT_RETRY_DELAY_S = 300.0
59
70
 
60
71
 
61
72
  def _setting(name: str) -> str:
@@ -202,10 +213,15 @@ async def fire(schedule: dict, *, client=None) -> dict:
202
213
  return out
203
214
 
204
215
  _running.add(sid)
216
+ # Kept so a transient lookup failure can re-arm the schedule (see below).
217
+ previous_fire = schedule.get("last_fire_at")
218
+ rearmed = False
205
219
  # Stamped BEFORE the run, not after: a long run, or one queued behind the
206
220
  # semaphore, would otherwise still look due on the next tick.
207
221
  schedules.mark_fired(sid)
208
222
  started = time.monotonic()
223
+ run_started = datetime.now(timezone.utc)
224
+ ticket = None # sdk.schedule_tickets.RunTicket when the schedule opts in
209
225
 
210
226
  try:
211
227
  if schedule.get("kind") == "plan":
@@ -222,9 +238,16 @@ async def fire(schedule: dict, *, client=None) -> dict:
222
238
  out["status"] = "inactive"
223
239
  return out
224
240
 
241
+ from sdk import schedule_tickets
225
242
  from sdk.agents import activity
226
243
  from sdk.agents.chat import AgentLookupUnavailable, run_agent
227
244
 
245
+ # Opened only once the agent is known to be runnable, so a disabled agent
246
+ # never leaves a ticket behind. Best-effort: no ticket never blocks a run.
247
+ # A retry of the same run reuses the ticket its first attempt opened.
248
+ ticket = (schedule_tickets.from_record(schedule.get("open_ticket"))
249
+ or await schedule_tickets.open_ticket(client, schedule, run_started))
250
+
228
251
  trigger = f"schedule · {schedule.get('cron')} ({schedule.get('timezone')})"
229
252
  try:
230
253
  result = await run_agent(
@@ -247,18 +270,46 @@ async def fire(schedule: dict, *, client=None) -> dict:
247
270
  # auto-disable — a few flaky minutes would otherwise switch off every
248
271
  # schedule on the box.
249
272
  transient = isinstance(exc, AgentLookupUnavailable)
273
+ attempt = int(schedule.get("transient_attempts") or 0) + 1
274
+ if transient and attempt <= MAX_TRANSIENT_RETRIES:
275
+ # The backend could not be READ: nothing ran. Put last_fire_at back
276
+ # so the schedule is due again, and hold it off for a few minutes,
277
+ # keeping the ticket open for the retry to reuse.
278
+ retry_at = datetime.now(timezone.utc).timestamp() + TRANSIENT_RETRY_DELAY_S
279
+ schedules.update(
280
+ sid, last_fire_at=previous_fire, transient_attempts=attempt,
281
+ retry_after=datetime.fromtimestamp(retry_at, timezone.utc)
282
+ .isoformat(timespec="seconds"),
283
+ open_ticket=schedule_tickets.to_record(ticket),
284
+ )
285
+ rearmed = True
286
+ schedules.mark_result(
287
+ sid, ok=True,
288
+ status=_with_ticket(
289
+ f"retrying {attempt}/{MAX_TRANSIENT_RETRIES} in "
290
+ f"{int(TRANSIENT_RETRY_DELAY_S // 60)} min — {exc}", ticket, ""),
291
+ )
292
+ logger.warning("scheduler.lookup_retry agent=%s attempt=%s err=%s",
293
+ agent, attempt, exc)
294
+ out["status"] = "retrying"
295
+ return out
250
296
  result = {"ok": False, "error": str(exc), "result": "",
251
297
  "input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0,
252
298
  "files_changed": []}
253
299
  path = run_log.start_run(agent, kind="scheduled", trigger=trigger,
254
300
  scope=str(schedule.get("scope") or "custom"))
255
301
  run_log.finish_run(path, result)
302
+ await schedule_tickets.close_ticket(client, ticket, ok=False, reason=str(exc))
256
303
  schedules.mark_result(
257
304
  sid,
258
305
  ok=transient, # transient → no strike, but still reported
259
- status=(f"retrying — {exc}" if transient else f"failed — {exc}"),
306
+ status=_with_ticket(
307
+ f"gave up after {MAX_TRANSIENT_RETRIES} retries — {exc}" if transient
308
+ else f"failed — {exc}", ticket, "backlog"),
260
309
  output_file=path.name if path else "",
261
- delivered_to=schedules.deliver(schedule, path),
310
+ # A failure record is never exported: the schedule's folder is
311
+ # where the person reads RESULTS (e.g. one research report a day).
312
+ delivered_to="",
262
313
  )
263
314
  logger.warning("scheduler.run_unrunnable agent=%s transient=%s err=%s",
264
315
  agent, transient, exc)
@@ -268,15 +319,29 @@ async def fire(schedule: dict, *, client=None) -> dict:
268
319
  elapsed = time.monotonic() - started
269
320
  ok = bool(result.get("ok"))
270
321
  latest = run_log.list_runs(agent, limit=1)
322
+ record = latest[0] if latest else None
323
+ # Only a successful run is exported; a failure stays in the agent's own
324
+ # run history, so the export folder never holds an error as a "report".
325
+ delivered_to = schedules.deliver(
326
+ schedule, record,
327
+ list(result.get("files_changed") or []),
328
+ workspace=_agent_workspace(agent),
329
+ ) if ok else ""
330
+ ticket_status = ""
331
+ if ticket is not None:
332
+ output, missing = schedule_tickets.evidence_path(
333
+ schedule, record, delivered_to, run_started)
334
+ reason = (str(result.get("error") or "the agent reported a failure")
335
+ if not ok else missing)
336
+ ticket_status = await schedule_tickets.close_ticket(
337
+ client, ticket, ok=ok and output is not None, reason=reason, output=output)
271
338
  schedules.mark_result(
272
339
  sid, ok=ok,
273
- status="ok" if ok else f"failed — {str(result.get('error') or '')[:200]}",
274
- output_file=latest[0].name if latest else "",
275
- delivered_to=schedules.deliver(
276
- schedule, latest[0] if latest else None,
277
- list(result.get("files_changed") or []),
278
- workspace=_agent_workspace(agent),
279
- ),
340
+ status=_with_ticket(
341
+ "ok" if ok else f"failed — {str(result.get('error') or '')[:200]}",
342
+ ticket, ticket_status),
343
+ output_file=record.name if record else "",
344
+ delivered_to=delivered_to,
280
345
  )
281
346
  # One call writes BOTH the local usage ledger and the backend
282
347
  # agent_activities feed, and never raises.
@@ -287,10 +352,19 @@ async def fire(schedule: dict, *, client=None) -> dict:
287
352
 
288
353
  except Exception as exc: # noqa: BLE001 — a bad run must never end the loop
289
354
  logger.warning("scheduler.fire_failed agent=%s err=%s", agent, exc)
290
- schedules.mark_result(sid, ok=False, status=f"failed — {exc}")
355
+ if ticket is not None:
356
+ try:
357
+ from sdk import schedule_tickets
358
+ await schedule_tickets.close_ticket(client, ticket, ok=False, reason=str(exc))
359
+ except Exception: # noqa: BLE001 — never let ticket bookkeeping escape
360
+ pass
361
+ schedules.mark_result(sid, ok=False, status=_with_ticket(f"failed — {exc}", ticket, "backlog"))
291
362
  out["status"] = "failed"
292
363
  return out
293
364
  finally:
365
+ if not rearmed and any(schedule.get(k) for k in ("transient_attempts", "retry_after", "open_ticket")):
366
+ # The run reached a real outcome: clear any retry state it carried.
367
+ schedules.update(sid, transient_attempts=0, retry_after="", open_ticket=None)
294
368
  _running.discard(sid)
295
369
  release_lock(agent)
296
370
  try:
@@ -299,6 +373,14 @@ async def fire(schedule: dict, *, client=None) -> dict:
299
373
  pass
300
374
 
301
375
 
376
+ def _with_ticket(status: str, ticket, ticket_status: str) -> str:
377
+ """Append the run's ticket to the schedule's status line, when it has one."""
378
+ if ticket is None:
379
+ return status
380
+ key = ticket.key or ticket.task_id
381
+ return f"{status} · ticket {key}" + (f" → {ticket_status}" if ticket_status else "")
382
+
383
+
302
384
  def _agent_workspace(agent: str) -> Path | None:
303
385
  """The directory the agent ran in — where any file it wrote lives."""
304
386
  try:
@@ -371,9 +453,13 @@ async def _fire_guarded(schedule: dict) -> None:
371
453
  global _semaphore
372
454
  if _semaphore is None:
373
455
  _semaphore = asyncio.Semaphore(concurrency())
374
- async with _semaphore:
375
- await asyncio.sleep(random.uniform(0, _MAX_JITTER_S))
376
- await fire(schedule)
456
+ sid = str(schedule.get("schedule_id") or "")
457
+ try:
458
+ async with _semaphore:
459
+ await asyncio.sleep(random.uniform(0, _MAX_JITTER_S))
460
+ await fire(schedule)
461
+ finally:
462
+ _queued.discard(sid)
377
463
 
378
464
 
379
465
  # ── The loop ────────────────────────────────────────────────────────────────
@@ -383,13 +469,30 @@ def due_now(now: datetime | None = None) -> list[dict]:
383
469
  edit in the panel takes effect on the next tick with no restart."""
384
470
  now = now or datetime.now(timezone.utc)
385
471
  grace = grace_s()
386
- return [s for s in schedules.list_schedules() if schedules.is_due(s, now, grace)]
472
+ return [s for s in schedules.list_schedules()
473
+ if schedules.is_due(s, now, grace) and not _held_off(s, now)]
474
+
475
+
476
+ def _held_off(schedule: dict, now: datetime) -> bool:
477
+ """True while a re-armed schedule is waiting out its retry delay."""
478
+ raw = schedule.get("retry_after")
479
+ if not raw:
480
+ return False
481
+ try:
482
+ until = datetime.fromisoformat(str(raw))
483
+ except ValueError:
484
+ return False
485
+ if until.tzinfo is None:
486
+ until = until.replace(tzinfo=timezone.utc)
487
+ return now < until
387
488
 
388
489
 
389
490
  async def tick(now: datetime | None = None) -> int:
390
491
  """One evaluation pass. Returns how many runs were dispatched."""
391
- ready = due_now(now)
492
+ ready = [s for s in due_now(now)
493
+ if str(s.get("schedule_id") or "") not in _queued]
392
494
  for schedule in ready:
495
+ _queued.add(str(schedule.get("schedule_id") or ""))
393
496
  asyncio.ensure_future(_fire_guarded(schedule))
394
497
  return len(ready)
395
498
 
@@ -429,6 +532,7 @@ def status() -> dict:
429
532
  "total": len(rows),
430
533
  "active": sum(1 for s in rows if s.get("enabled", True)),
431
534
  "in_flight": sorted(_running),
535
+ "queued": sorted(_queued - _running),
432
536
  "next": [
433
537
  {
434
538
  "schedule_id": s.get("schedule_id"),