diff --git a/scripts/build_skills_index.py b/scripts/build_skills_index.py index a5bf900d831..064fabd1d57 100644 --- a/scripts/build_skills_index.py +++ b/scripts/build_skills_index.py @@ -272,8 +272,9 @@ def main(): # (well above current catalog size) lets the full catalog land in the # index instead of being truncated at an arbitrary build-time limit. SOURCE_LIMITS = { - # ClawHub had 49,698+ skills as of May 2026; 200k leaves headroom. - "clawhub": 200_000, + # 0 = unbounded catalog walk (max_items=0 in ClawHubSource). A positive + # limit bounds the walk and also enables the interactive 12s budget. + "clawhub": 0, "lobehub": 100_000, "browse-sh": 5_000, "claude-marketplace": 5_000, diff --git a/tests/tools/test_skills_hub_clawhub.py b/tests/tools/test_skills_hub_clawhub.py index 972175999fd..6ac434dd207 100644 --- a/tests/tools/test_skills_hub_clawhub.py +++ b/tests/tools/test_skills_hub_clawhub.py @@ -381,9 +381,10 @@ class TestClawHubSource(unittest.TestCase): mock_get.side_effect = side_effect - # Force the deadline to be in the past immediately. + # Force the deadline to be in the past immediately. Budget only applies + # to bounded browse walks (max_items > 0), not the index builder path. with patch.object(ClawHubSource, "CATALOG_WALK_BUDGET_SECONDS", -1): - results = self.src._load_catalog_index() + results = self.src._load_catalog_index(max_items=10) # Walk broke well before the 750-page cap. self.assertLess(page_calls["n"], 750) @@ -480,6 +481,23 @@ class TestClawHubCatalogWalkBounded(unittest.TestCase): # Partial (bounded) walk must not be cached. mock_write_cache.assert_not_called() + @patch("tools.skills_hub._write_index_cache") + @patch("tools.skills_hub._read_index_cache", return_value=None) + @patch("tools.skills_hub.httpx.get") + def test_max_items_zero_ignores_wall_clock_budget( + self, mock_get, _mock_read_cache, _mock_write_cache + ): + """Index builder path (max_items=0) must not truncate on the browse budget.""" + page_calls = {"n": 0} + mock_get.side_effect = self._infinite_pages(page_calls) + + with patch.object(ClawHubSource, "CATALOG_WALK_BUDGET_SECONDS", -1): + results = self.src._load_catalog_index(max_items=0) + + # No budget -> walks until the 750-page safety cap, not ~14 pages in 12s. + self.assertEqual(page_calls["n"], 750) + self.assertEqual(len(results), 750) + @patch("tools.skills_hub._write_index_cache") @patch("tools.skills_hub._read_index_cache", return_value=None) @patch("tools.skills_hub.httpx.get") diff --git a/tools/skills_hub.py b/tools/skills_hub.py index 7750eb1b96e..cf99b5b9d29 100644 --- a/tools/skills_hub.py +++ b/tools/skills_hub.py @@ -2279,12 +2279,20 @@ class ClawHubSource(SkillSource): # terminates well before this on `nextCursor` going None — the cap is # a safety rail against an infinite-cursor loop. max_pages = 750 - deadline = time.monotonic() + self.CATALOG_WALK_BUDGET_SECONDS + # Wall-clock budget is for interactive browse (max_items > 0) only. + # The offline index builder passes max_items=0 and must walk the full + # catalog — a 12s cap there ships ~3k skills and trips the deploy + # health floor (20k). + deadline = ( + time.monotonic() + self.CATALOG_WALK_BUDGET_SECONDS + if max_items > 0 + else None + ) hit_deadline = False hit_max_items = False for _ in range(max_pages): - if time.monotonic() > deadline: + if deadline is not None and time.monotonic() > deadline: hit_deadline = True break params: Dict[str, Any] = {"limit": 200}