diff --git a/.github/workflows/cloud-agent.yml b/.github/workflows/cloud-agent.yml index b34a94e..d7e50c3 100644 --- a/.github/workflows/cloud-agent.yml +++ b/.github/workflows/cloud-agent.yml @@ -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' diff --git a/scripts/agent_radar.py b/scripts/agent_radar.py index ecab778..c42d6c6 100644 --- a/scripts/agent_radar.py +++ b/scripts/agent_radar.py @@ -58,7 +58,7 @@ INIT_PROTECTED_DIRS = ("prompts", "automation", "docs") -__version__ = "0.20.0" +__version__ = "0.20.1" CORE_FILES = [ "README.md", diff --git a/scripts/cloud_agent_runner.py b/scripts/cloud_agent_runner.py index fc51aa6..3a96d2c 100644 --- a/scripts/cloud_agent_runner.py +++ b/scripts/cloud_agent_runner.py @@ -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: @@ -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] = {} @@ -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: diff --git a/tests/test_cloud_agent_runner.py b/tests/test_cloud_agent_runner.py index 250cb0e..fca8924 100644 --- a/tests/test_cloud_agent_runner.py +++ b/tests/test_cloud_agent_runner.py @@ -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"]) @@ -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.