Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/cloud-agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ jobs:
AI_GATEWAY_FALLBACK_MODELS: ${{ vars.AI_GATEWAY_FALLBACK_MODELS || 'google/gemini-2.5-flash-lite,openai/gpt-5-nano' }}
AI_GATEWAY_SCREEN_FALLBACK_MODELS: ${{ vars.AI_GATEWAY_SCREEN_FALLBACK_MODELS || 'google/gemini-2.5-flash-lite,openai/gpt-oss-120b' }}
AI_GATEWAY_MAX_OUTPUT_TOKENS: ${{ vars.AI_GATEWAY_MAX_OUTPUT_TOKENS || '32768' }}
# Free-tier quota mode: merge the 4 screening lanes into 2 calls.
SCREENING_SHARD_GROUPS: ${{ vars.SCREENING_SHARD_GROUPS || '2' }}
# Tuned in-repo (repo Actions variables held stale lower values and
# silently clamped collection breadth; see docs/release-v0.19.0.md).
MAX_RELEASE_REPOS: '32'
Expand Down
2 changes: 1 addition & 1 deletion scripts/agent_radar.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
INIT_PROTECTED_DIRS = ("prompts", "automation", "docs")


__version__ = "0.20.0"
__version__ = "0.20.1"

CORE_FILES = [
"README.md",
Expand Down
29 changes: 26 additions & 3 deletions scripts/cloud_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -852,10 +852,14 @@ def run_cli(root: Path, command: str, day: dt.date) -> None:


def auto_tasks(day: dt.date) -> list[str]:
tasks = ["daily", "source-sweep"]
tasks = ["daily"]
# Free-quota budget (2026-07-22): auxiliary tasks produce slow-moving
# assets (research-log candidates, source discovery, promotions) and no
# longer run daily. The daily report keeps its daily cadence.
if day.weekday() in {0, 3}: # Mon/Thu
tasks.append("source-sweep")
if day.weekday() == 6:
tasks.append("weekly")
if day.weekday() in {2, 6}:
tasks.append("promote-candidates")
# Mid-month refresh keeps the monthly from staying a day-1 seed until month end.
if day.day == 15 or (day + dt.timedelta(days=1)).month != day.month:
Expand Down Expand Up @@ -2149,6 +2153,14 @@ def validate_daily_append_size(rel_path: str, mode: str, content: str) -> None:
)


# Pairwise merges applied when SCREENING_SHARD_GROUPS=2 (free-tier quota mode):
# discussion keeps first position so merge dedup preserves community framing.
SCREENING_SHARD_MERGE_2: tuple[tuple[str, tuple[str, ...]], ...] = (
("discussion-official", ("discussion", "official-vendor")),
("oss-packages", ("github-oss", "packages")),
)


def screening_shard_items(items: list[dict[str, str]]) -> list[tuple[str, list[dict[str, str]]]]:
"""Split scored items into per-lane-group shards for separate screening."""
lane_to_shard: dict[str, str] = {}
Expand All @@ -2160,7 +2172,18 @@ def screening_shard_items(items: list[dict[str, str]]) -> list[tuple[str, list[d
lane = source_lane(item.get("source", ""))
shard_name = lane_to_shard.get(lane, "official-vendor")
buckets[shard_name].append(item)
return [(name, buckets[name]) for name, _ in SCREENING_SHARD_LANES if buckets[name]]
shards = [(name, buckets[name]) for name, _ in SCREENING_SHARD_LANES if buckets[name]]
if env_int("SCREENING_SHARD_GROUPS", 4) <= 2:
merged: list[tuple[str, list[dict[str, str]]]] = []
by_name = dict(shards)
for group_name, members in SCREENING_SHARD_MERGE_2:
combined: list[dict[str, str]] = []
for member in members:
combined.extend(by_name.get(member, []))
if combined:
merged.append((group_name, combined))
return merged
return shards


def candidate_dedupe_key(candidate: dict[str, Any]) -> str:
Expand Down
42 changes: 36 additions & 6 deletions tests/test_cloud_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,13 @@ def test_ai_gateway_default_fallbacks_are_tiered_by_workload(self) -> None:
def test_auto_tasks_include_candidate_promotion_on_sunday(self) -> None:
tasks = cloud_agent_runner.auto_tasks(cloud_agent_runner.parse_date("2026-07-05"))
self.assertIn("daily", tasks)
self.assertIn("source-sweep", tasks)
self.assertIn("weekly", tasks)
self.assertIn("promote-candidates", tasks)

def test_auto_tasks_promote_twice_a_week(self) -> None:
def test_auto_tasks_weekday_is_daily_only(self) -> None:
# v0.20.1 free-quota schedule: a plain Wednesday runs only the daily.
tasks = cloud_agent_runner.auto_tasks(cloud_agent_runner.parse_date("2026-07-01"))
self.assertIn("daily", tasks)
self.assertIn("source-sweep", tasks)
self.assertIn("promote-candidates", tasks)
self.assertNotIn("weekly", tasks)
self.assertEqual(tasks, ["daily"])

def test_daily_can_update_source_registry(self) -> None:
self.assertIn("sources.md", cloud_agent_runner.TASK_CONFIG["daily"]["allowed"])
Expand Down Expand Up @@ -2504,6 +2501,39 @@ def test_ai_gateway_accepts_fenced_json_without_fallback(self) -> None:
self.assertEqual(json.loads(cloud_agent_runner.response_output_text(parsed))["summary"], "ok")
urlopen_mock.assert_called_once()

def test_auto_tasks_free_quota_schedule(self) -> None:
# v0.20.1: daily every day; source-sweep Mon/Thu; promote Sun; weekly Sun.
mon = cloud_agent_runner.parse_date("2026-07-20")
thu = cloud_agent_runner.parse_date("2026-07-23")
sun = cloud_agent_runner.parse_date("2026-07-26")
tue = cloud_agent_runner.parse_date("2026-07-21")
self.assertIn("source-sweep", cloud_agent_runner.auto_tasks(mon))
self.assertIn("source-sweep", cloud_agent_runner.auto_tasks(thu))
self.assertNotIn("source-sweep", cloud_agent_runner.auto_tasks(tue))
sun_tasks = cloud_agent_runner.auto_tasks(sun)
self.assertIn("weekly", sun_tasks)
self.assertIn("promote-candidates", sun_tasks)
for d in (mon, tue, thu, sun):
self.assertIn("daily", cloud_agent_runner.auto_tasks(d))
mid = cloud_agent_runner.parse_date("2026-07-15")
self.assertIn("monthly", cloud_agent_runner.auto_tasks(mid))

def test_screening_shard_groups_merge_to_two(self) -> None:
items = [
{"source": "reddit-rss:LocalLLaMA", "title": "a", "url": "u1", "note": ""},
{"source": "openai-blog", "title": "b", "url": "u2", "note": ""},
{"source": "github", "title": "c", "url": "u3", "note": ""},
{"source": "npm", "title": "d", "url": "u4", "note": ""},
]
with mock.patch.dict(os.environ, {"SCREENING_SHARD_GROUPS": "2"}, clear=False):
shards = cloud_agent_runner.screening_shard_items(items)
names = [name for name, _ in shards]
self.assertEqual(names, ["discussion-official", "oss-packages"])
self.assertEqual(sum(len(x) for _, x in shards), 4)
with mock.patch.dict(os.environ, {"SCREENING_SHARD_GROUPS": "4"}, clear=False):
shards4 = cloud_agent_runner.screening_shard_items(items)
self.assertGreaterEqual(len(shards4), 3)

def test_ai_gateway_429_walks_rounds_with_backoff(self) -> None:
# Issue #76: one pass over a same-pool chain died on free-tier 429s.
# The loop must walk the chain for several rounds with real backoff.
Expand Down
Loading