devops-bot-sdk 1.5.0__tar.gz → 1.6.2__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 (107) hide show
  1. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/__init__.py +2 -2
  4. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/github_ingest.py +66 -3
  5. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/run_auto.py +41 -0
  6. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_github_ingest.py +72 -3
  7. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/README.md +0 -0
  8. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  9. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  10. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  11. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/devops_bot_sdk.egg-info/requires.txt +0 -0
  12. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  13. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/pyproject.toml +0 -0
  14. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agentd.py +0 -0
  15. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/__init__.py +0 -0
  16. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/activity.py +0 -0
  17. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/builder.py +0 -0
  18. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/chat.py +0 -0
  19. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/connectors.py +0 -0
  20. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/engines/__init__.py +0 -0
  21. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/engines/claude_code.py +0 -0
  22. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/engines/http.py +0 -0
  23. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/engines/mcp.py +0 -0
  24. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/engines/n8n.py +0 -0
  25. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/engines/shell.py +0 -0
  26. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/integrations.py +0 -0
  27. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/inventory.py +0 -0
  28. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/library/__init__.py +0 -0
  29. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/manifest.py +0 -0
  30. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/prompt_studio.py +0 -0
  31. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/prompt_template.py +0 -0
  32. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/prompt_vars.py +0 -0
  33. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/run_log.py +0 -0
  34. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/secrets.py +0 -0
  35. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/ticket_run.py +0 -0
  36. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/usage.py +0 -0
  37. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/agents/varstore.py +0 -0
  38. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/cli.py +0 -0
  39. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/client.py +0 -0
  40. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/collectors/__init__.py +0 -0
  41. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/collectors/files.py +0 -0
  42. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/collectors/process.py +0 -0
  43. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/collectors/screenshot.py +0 -0
  44. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/config.py +0 -0
  45. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/crucial.py +0 -0
  46. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/design_verify/__init__.py +0 -0
  47. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/design_verify/assets.py +0 -0
  48. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/design_verify/bootstrap.py +0 -0
  49. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/design_verify/browser.py +0 -0
  50. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/design_verify/compare.py +0 -0
  51. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/design_verify/loop.py +0 -0
  52. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/exceptions.py +0 -0
  53. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/git_ops.py +0 -0
  54. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/graphify.py +0 -0
  55. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/hooks/__init__.py +0 -0
  56. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/hooks/crucial_guard.py +0 -0
  57. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ipc/__init__.py +0 -0
  58. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ipc/electron_bridge.py +0 -0
  59. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ipc/handlers.py +0 -0
  60. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/local_exec.py +0 -0
  61. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/models/__init__.py +0 -0
  62. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/models/envelope.py +0 -0
  63. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/models/requests.py +0 -0
  64. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/models/responses.py +0 -0
  65. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/models/snapshots.py +0 -0
  66. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/platform_compat.py +0 -0
  67. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/py.typed +0 -0
  68. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/sse.py +0 -0
  69. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/test.py +0 -0
  70. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/test_pipeline.py +0 -0
  71. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/__init__.py +0 -0
  72. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/api.py +0 -0
  73. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/dashboard.py +0 -0
  74. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/health.py +0 -0
  75. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/ip_allowlist.py +0 -0
  76. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/jobs.py +0 -0
  77. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/models.py +0 -0
  78. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/runner.py +0 -0
  79. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/security.py +0 -0
  80. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/server.py +0 -0
  81. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/service.py +0 -0
  82. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/session.py +0 -0
  83. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/setup_manager.py +0 -0
  84. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/agents.html +0 -0
  85. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/app.js +0 -0
  86. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/custom-agents.html +0 -0
  87. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/default-agents.html +0 -0
  88. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/index.html +0 -0
  89. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/login.html +0 -0
  90. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/runs.html +0 -0
  91. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/setup.html +0 -0
  92. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/styles.css +0 -0
  93. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/ui/static/tickets.html +0 -0
  94. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/updater.py +0 -0
  95. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/sdk/vps_health.py +0 -0
  96. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/setup.cfg +0 -0
  97. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_agent_activity.py +0 -0
  98. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_agent_chat_features.py +0 -0
  99. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_agent_prompt_async.py +0 -0
  100. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_agent_prompt_models.py +0 -0
  101. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_agent_run_ticket.py +0 -0
  102. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_agents_connectors_builder.py +0 -0
  103. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_git_action.py +0 -0
  104. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_health.py +0 -0
  105. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_otp_login.py +0 -0
  106. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_updater_extras.py +0 -0
  107. {devops_bot_sdk-1.5.0 → devops_bot_sdk-1.6.2}/tests/test_vps_health.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.5.0
3
+ Version: 1.6.2
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.5.0
3
+ Version: 1.6.2
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
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.5.0
3
+ Version: 1.6.2
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.5.0" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
33
+ __version__ = "1.6.2" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
34
34
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
35
35
 
36
36
  from sdk.client import BackendClient
@@ -13,6 +13,16 @@ What moves and what does NOT:
13
13
  Postgres, Qdrant, or Azure, and holds no credential beyond the ``co_`` token
14
14
  it was configured with. One VPS cannot write another user's vectors.
15
15
 
16
+ **Two things run, both on by default, no configuration needed:**
17
+ 1. a ONE-TIME 50-day history backfill, fired from ``agentos-auto update`` via
18
+ :func:`run_backfill_once` and guarded by ``BACKFILL_ID`` so it happens at
19
+ most once per machine;
20
+ 2. the DAILY loop below, started by the ``agentos-ui`` lifespan alongside the
21
+ 2-hourly ``sdk.vps_health`` ping.
22
+
23
+ The daily loop also covers the backfill: if a box never runs ``agentos-auto
24
+ update``, the first tick sees ``backfill_pending`` and seeds the history itself.
25
+
16
26
  Schedule semantics — a real daily slot, not a naive interval:
17
27
  * the loop ticks every ``_TICK_S`` (cheap: no network unless a run is due),
18
28
  * a run is DUE when the UTC clock has passed ``AGENTOS_GITHUB_INGEST_AT``
@@ -31,7 +41,9 @@ State lives in ``~/.agentos/github_ingest.json``: ``last_success_at`` (the
31
41
  watermark), plus the last attempt's outcome for ``GET /api/ui/github-ingest``.
32
42
 
33
43
  Config — process env OR ``~/.agentos/.env`` (process env wins), all optional:
34
- ``AGENTOS_GITHUB_INGEST`` ``0``/``false``/``no``/``off`` disables the cron.
44
+ ``AGENTOS_GITHUB_INGEST`` ``0``/``false``/``no``/``off`` disables everything.
45
+ ``AGENTOS_GITHUB_INGEST_DAILY`` ``0`` turns the recurring daily loop OFF (default ON).
46
+ ``AGENTOS_GITHUB_INGEST_BACKFILL_DAYS`` one-time seed size (default 50, max 365).
35
47
  ``AGENTOS_GITHUB_INGEST_AT`` daily UTC slot, ``HH:MM`` (default ``13:30``).
36
48
  ``AGENTOS_GITHUB_INGEST_LOOKBACK_DAYS`` catch-up cap (default 7, max 90).
37
49
 
@@ -88,10 +100,24 @@ def _setting(name: str) -> str:
88
100
 
89
101
 
90
102
  def enabled() -> bool:
91
- """True unless ``AGENTOS_GITHUB_INGEST`` is explicitly falsey (opt-OUT)."""
103
+ """Master switch — False only when ``AGENTOS_GITHUB_INGEST`` is explicitly falsey.
104
+
105
+ Gates BOTH the one-time backfill and the daily loop.
106
+ """
92
107
  return _setting("AGENTOS_GITHUB_INGEST").lower() not in _FALSEY
93
108
 
94
109
 
110
+ def daily_enabled() -> bool:
111
+ """Is the RECURRING daily loop switched on? **Default: yes.**
112
+
113
+ The steady state is two schedules running side by side on every box, with no
114
+ configuration required: ``sdk.vps_health`` every 2 hours, and this one daily
115
+ at :func:`slot`. Opt OUT with ``AGENTOS_GITHUB_INGEST_DAILY=0`` (or turn off
116
+ GitHub ingestion entirely with ``AGENTOS_GITHUB_INGEST=0``).
117
+ """
118
+ return _setting("AGENTOS_GITHUB_INGEST_DAILY").lower() not in _FALSEY
119
+
120
+
95
121
  def slot() -> tuple[int, int]:
96
122
  """The daily UTC slot as ``(hour, minute)``. Bad values fall back to the default."""
97
123
  raw = _setting("AGENTOS_GITHUB_INGEST_AT") or DEFAULT_AT
@@ -307,6 +333,7 @@ def status() -> dict:
307
333
  state = load_state()
308
334
  return {
309
335
  "enabled": enabled(),
336
+ "daily_loop": daily_enabled(),
310
337
  "slot_utc": f"{hour:02d}:{minute:02d}",
311
338
  "lookback_days": lookback_days(),
312
339
  "backfill_days": backfill_days(),
@@ -338,8 +365,44 @@ async def run_forever(tick_s: float = _TICK_S) -> None:
338
365
 
339
366
 
340
367
  def start() -> "asyncio.Task | None":
341
- """Start the cron as a background task when enabled. Returns the task (or None)."""
368
+ """Start the RECURRING daily loop. On by default; returns the task, or None
369
+ when GitHub ingestion (or just the daily loop) has been switched off."""
342
370
  if not enabled():
343
371
  logger.info("github_ingest.disabled (AGENTOS_GITHUB_INGEST is off)")
344
372
  return None
373
+ if not daily_enabled():
374
+ logger.info("github_ingest.daily_loop_off (AGENTOS_GITHUB_INGEST_DAILY is off)")
375
+ return None
345
376
  return asyncio.ensure_future(run_forever())
377
+
378
+
379
+ # ── The one-time backfill (what `agentos-auto update` fires) ────────────────
380
+
381
+ def run_backfill_once(client: "BackendClient | None" = None) -> dict:
382
+ """Run THIS rollout's 50-day history backfill — at most once per machine.
383
+
384
+ Called from ``agentos-auto update``. Safe to call on every update: the
385
+ ``BACKFILL_ID`` marker in ``~/.agentos/github_ingest.json`` means the second
386
+ and later calls are a no-op, so a user who updates weekly still ingests the
387
+ history exactly once.
388
+
389
+ Synchronous by design — the update command is a plain CLI flow, not an event
390
+ loop. Returns ``{ran, status, since, job_id, status_url, reason}``;
391
+ ``ran=False`` with ``status="already_done"`` when the marker is already set,
392
+ or ``"disabled"`` when the feature is switched off. Never raises: a failed
393
+ backfill must not fail the update.
394
+ """
395
+ if not enabled():
396
+ return {"ran": False, "status": "disabled", "reason": "AGENTOS_GITHUB_INGEST is off"}
397
+
398
+ state = load_state()
399
+ if not backfill_pending(state):
400
+ return {"ran": False, "status": "already_done",
401
+ "since": state.get("last_since", ""), "job_id": state.get("last_job_id", "")}
402
+
403
+ try:
404
+ result = asyncio.run(run_once(client, days=backfill_days()))
405
+ except Exception as exc: # noqa: BLE001 — never fail `agentos-auto update`
406
+ logger.warning("github_ingest.backfill_failed err=%s", exc)
407
+ return {"ran": True, "status": "failed", "reason": str(exc)[:200]}
408
+ return {"ran": True, **result}
@@ -588,6 +588,47 @@ def main() -> None:
588
588
  "`agentos-auto stop` then `agentos-auto` to run the new one.")
589
589
  else:
590
590
  print(f"already up to date (v{after})")
591
+
592
+ # One-time GitHub history backfill — the last 50 days of THIS user's PRs.
593
+ # Fired from `update` because that is the moment every machine passes
594
+ # through exactly once per rollout. Guarded by the BACKFILL_ID marker in
595
+ # ~/.agentos/github_ingest.json, so a second `update` is a no-op — and it
596
+ # runs whether or not the version actually changed, so a box that was
597
+ # already current still seeds its history. Never fails the update.
598
+ try:
599
+ from sdk import github_ingest
600
+
601
+ gh = github_ingest.run_backfill_once()
602
+ status = gh.get("status")
603
+ if gh.get("ran") and status == "queued":
604
+ print(f"✓ GitHub history: queued PRs since {gh.get('since')} "
605
+ f"(job {gh.get('job_id') or 'n/a'})")
606
+ elif gh.get("ran") and status == "no_github_link":
607
+ print(" GitHub history: skipped — no GitHub account connected")
608
+ elif gh.get("ran"):
609
+ print(f" GitHub history: not queued — {gh.get('reason') or status}")
610
+ elif status == "already_done":
611
+ print(" GitHub history: already ingested")
612
+ except Exception as exc: # noqa: BLE001 — never fail the update on this
613
+ print(f" GitHub history: skipped ({exc})")
614
+
615
+ # Refresh this machine's health immediately. The panel pings every 2h on
616
+ # its own, but an update restarts the panel — so without this the
617
+ # allocation row can look stale for up to two hours right after a rollout,
618
+ # which is exactly when someone is checking whether the box came back.
619
+ try:
620
+ import asyncio as _asyncio
621
+
622
+ from sdk import vps_health
623
+
624
+ if vps_health.enabled():
625
+ if _asyncio.run(vps_health.report_once()):
626
+ print("✓ VPS health: reported")
627
+ else:
628
+ last = (vps_health.last_report().get("last") or {})
629
+ print(f" VPS health: not reported — {last.get('reason') or 'unknown'}")
630
+ except Exception as exc: # noqa: BLE001 — never fail the update on this
631
+ print(f" VPS health: skipped ({exc})")
591
632
  return
592
633
 
593
634
  if cmd == "stop":
@@ -24,7 +24,8 @@ def isolated_state(tmp_path, monkeypatch):
24
24
  monkeypatch.setattr(gi, "STATE_FILE", tmp_path / "github_ingest.json")
25
25
  import sdk.config as cfg
26
26
  monkeypatch.setattr(cfg, "_dotenv_value", lambda key: "")
27
- for var in ("AGENTOS_GITHUB_INGEST", "AGENTOS_GITHUB_INGEST_AT",
27
+ for var in ("AGENTOS_GITHUB_INGEST", "AGENTOS_GITHUB_INGEST_DAILY",
28
+ "AGENTOS_GITHUB_INGEST_AT",
28
29
  "AGENTOS_GITHUB_INGEST_LOOKBACK_DAYS",
29
30
  "AGENTOS_GITHUB_INGEST_BACKFILL_DAYS"):
30
31
  monkeypatch.delenv(var, raising=False)
@@ -150,6 +151,14 @@ class TestSinceWindow:
150
151
  _UNSET = object() # so an explicit None ("no link" / "call failed") is distinguishable
151
152
 
152
153
 
154
+ def _sync_stub(client):
155
+ """Replace run_once with a coroutine that records the window it was asked for."""
156
+ async def _fake(c=None, *, now=None, days=None):
157
+ client.calls.append({"since_days": days})
158
+ return {"ok": True, "status": "queued", "since": "2026-06-14", "job_id": "job-1"}
159
+ return _fake
160
+
161
+
153
162
  class _FakeClient:
154
163
  def __init__(self, record=_UNSET, resp=_UNSET):
155
164
  self._record = {"username": "nouman", "accessToken": "gho_x"} if record is _UNSET else record
@@ -258,7 +267,23 @@ class TestState:
258
267
 
259
268
 
260
269
  class TestStart:
261
- def test_disabled_starts_nothing(self, monkeypatch):
270
+ def test_daily_loop_is_on_by_default(self):
271
+ """Steady state: 2-hourly VPS health + this daily loop, no config needed."""
272
+ assert gi.daily_enabled() is True
273
+
274
+ async def go():
275
+ task = gi.start()
276
+ assert task is not None
277
+ task.cancel()
278
+ try:
279
+ await task
280
+ except asyncio.CancelledError:
281
+ pass
282
+ return True
283
+
284
+ assert asyncio.run(go()) is True
285
+
286
+ def test_master_switch_stops_everything(self, monkeypatch):
262
287
  monkeypatch.setenv("AGENTOS_GITHUB_INGEST", "0")
263
288
 
264
289
  async def go():
@@ -266,7 +291,16 @@ class TestStart:
266
291
 
267
292
  assert asyncio.run(go()) is None
268
293
 
269
- def test_enabled_starts_a_task(self):
294
+ def test_daily_loop_can_be_opted_out(self, monkeypatch):
295
+ monkeypatch.setenv("AGENTOS_GITHUB_INGEST_DAILY", "0")
296
+
297
+ async def go():
298
+ return gi.start()
299
+
300
+ assert asyncio.run(go()) is None
301
+ assert gi.daily_enabled() is False
302
+
303
+ def test_loop_starts_and_cancels_cleanly(self):
270
304
  async def go():
271
305
  task = gi.start()
272
306
  assert task is not None
@@ -280,6 +314,41 @@ class TestStart:
280
314
  assert asyncio.run(go()) is True
281
315
 
282
316
 
317
+ class TestRunBackfillOnce:
318
+ """What `agentos-auto update` actually calls."""
319
+
320
+ def test_first_call_queues_50_days(self, monkeypatch):
321
+ client = _FakeClient()
322
+ monkeypatch.setattr(gi, "run_once", _sync_stub(client))
323
+ out = gi.run_backfill_once(client)
324
+ assert out["ran"] is True and out["status"] == "queued"
325
+ assert client.calls[0]["since_days"] == 50
326
+
327
+ def test_second_call_is_a_noop(self):
328
+ gi.save_state({"backfill_id": gi.BACKFILL_ID, "last_job_id": "job-1"})
329
+ out = gi.run_backfill_once(_FakeClient())
330
+ assert out["ran"] is False and out["status"] == "already_done"
331
+
332
+ def test_respects_the_master_switch(self, monkeypatch):
333
+ monkeypatch.setenv("AGENTOS_GITHUB_INGEST", "0")
334
+ out = gi.run_backfill_once(_FakeClient())
335
+ assert out["ran"] is False and out["status"] == "disabled"
336
+
337
+ def test_real_call_marks_it_done(self):
338
+ """End to end through run_once: queued, then the marker blocks a re-run."""
339
+ out = gi.run_backfill_once(_FakeClient())
340
+ assert out["ran"] is True and out["status"] == "queued"
341
+ assert gi.run_backfill_once(_FakeClient())["status"] == "already_done"
342
+
343
+ def test_never_raises_into_the_update(self, monkeypatch):
344
+ def boom(*a, **k):
345
+ raise RuntimeError("kaboom")
346
+
347
+ monkeypatch.setattr(gi, "run_once", boom)
348
+ out = gi.run_backfill_once(_FakeClient())
349
+ assert out["ran"] is True and out["status"] == "failed"
350
+
351
+
283
352
  # ── Routes ──────────────────────────────────────────────────────────────────
284
353
 
285
354
  fastapi = pytest.importorskip("fastapi")
File without changes
File without changes