Skip to content

fix(skills): honour overall_timeout and bound ClawHub catalog walk - #38481

Closed
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/skills-search-timeout-budget-38459
Closed

fix(skills): honour overall_timeout and bound ClawHub catalog walk#38481
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/skills-search-timeout-budget-38459

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes two structural bugs in the Skills Hub search path that let a single slow source stall the entire browse for minutes.

  1. parallel_search_sources never honoured overall_timeout. The ThreadPoolExecutor ran inside a with ... as pool: block. Even though as_completed(timeout=overall_timeout) raised TimeoutError on schedule, leaving the with block invokes ThreadPoolExecutor.__exit__shutdown(wait=True), which blocks until every submitted worker finishes. So a slow source (e.g. ClawHub) kept the caller blocked well past the budget. The executor is now managed manually and shut down with wait=False, cancel_futures=True in a finally, so the timeout returns promptly and not-yet-started work is dropped.

  2. ClawHub's catalog walk was unbounded in wall-clock and poisoned its cache on truncation. _load_catalog_index walked up to 750 sequential pages (each request under its own timeout=30, so nothing errored) and wrote the result to the index cache unconditionally. An interrupted or slow walk therefore cached a partial catalog. A CATALOG_WALK_BUDGET_SECONDS deadline now breaks the walk early, and the cache is written only when the walk reaches a natural stop (cursor exhausted or page cap) — never on a budget-truncated walk.

Related Issue

Fixes #38459

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • tools/skills_hub.pyparallel_search_sources: replace the with ThreadPoolExecutor(...) context manager with a manual executor + try/finally that calls shutdown(wait=False, cancel_futures=True).
  • tools/skills_hub.pyClawHubSource: add CATALOG_WALK_BUDGET_SECONDS = 12; in _load_catalog_index, add a time.monotonic() deadline that breaks the walk and only writes the cache when the walk was not truncated by the budget.
  • tests/tools/test_skills_hub.py — regression + happy-path tests for the timeout.
  • tests/tools/test_skills_hub_clawhub.py — regression + happy-path tests for the walk budget and cache-poisoning guard.

How to Test

  1. uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/tools/test_skills_hub.py tests/tools/test_skills_hub_clawhub.py -v — 155 pass.
  2. Regression proof (timeout): revert the parallel_search_sources hunk → test_slow_source_does_not_block_caller blocks ~5s and fails; restore → returns in <2s with the slow source in timed_out_ids.
  3. Regression proof (walk budget): revert the ClawHub hunks → test_catalog_walk_aborts_on_budget_and_does_not_poison_cache fails (no budget/guard, cache written on a truncated walk); restore → passes.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15 (Python 3.11)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Contract Protected

Invariant: parallel_search_sources returns within roughly overall_timeout, and the ClawHub catalog cache only ever holds a complete walk.

  • Known-bad inputs now covered:
    • A source whose search() blocks far past overall_timeout (5s sleep vs 0.3s budget) — the call now returns in <2s and flags the source in timed_out_ids instead of blocking on shutdown(wait=True).
    • A ClawHub endpoint that always advertises a nextCursor (would otherwise walk all 750 pages) — the wall-clock budget breaks the walk early.
    • A budget-truncated walk — _write_index_cache is not called, so a partial catalog never poisons the cache.
  • Future-input coverage: the deadline is wall-clock based, so it bounds the walk regardless of page count, page size, or per-request latency; the cache-write guard is keyed on why the loop exited (natural stop vs deadline), so any future early-exit added before a natural stop stays uncached unless explicitly marked complete.
  • Negative case: happy paths are asserted — fast sources complete with empty timed_out_ids, and a walk that exhausts its cursor within budget still writes the cache exactly once.

parallel_search_sources accepted an overall_timeout but never honoured it.
The ThreadPoolExecutor ran inside a `with ... as pool` block, whose __exit__
calls shutdown(wait=True); even after as_completed() raised TimeoutError on
schedule, leaving the block blocked the caller until every worker finished.
A single slow source (e.g. ClawHub) therefore stalled the entire browse for
minutes. Manage the executor manually and shut it down with
wait=False, cancel_futures=True in a finally, so the timeout actually returns
and not-yet-started work is dropped.

ClawHubSource._load_catalog_index walked up to 750 sequential pages with no
wall-clock bound (each request under its own timeout=30, so nothing errored),
and wrote the result to the index cache unconditionally — so an interrupted or
slow walk poisoned the cache with a partial catalog. Add a
CATALOG_WALK_BUDGET_SECONDS deadline that breaks the walk early, and only write
the cache when the walk reaches a natural stop (cursor exhausted or page cap),
never on a budget-truncated walk.

Adds regression tests covering both bugs (timeout honoured + slow source
flagged; budget abort does not poison cache) plus their happy-path invariants.
Copilot AI review requested due to automatic review settings June 3, 2026 21:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduce wall-clock safeguards to prevent long-running ClawHub catalog walks and ensure parallel_search_sources honors overall_timeout without blocking on slow sources.

Changes:

  • Add a wall-clock budget to ClawHub catalog walking and avoid caching partial walks.
  • Change parallel_search_sources to manually manage the executor and shutdown(wait=False) so timeouts are respected.
  • Add regression tests covering the catalog budget/caching behavior and timeout honoring in parallel search.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
tools/skills_hub.py Adds catalog walk budget/conditional caching and updates threadpool shutdown to honor overall timeout.
tests/tools/test_skills_hub_clawhub.py Adds tests to ensure partial catalog walks don’t poison the cache and natural termination does cache.
tests/tools/test_skills_hub.py Adds regression tests for parallel_search_sources timeout behavior using fake sources.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tools/skills_hub.py
Comment on lines +2191 to +2197
deadline = time.monotonic() + self.CATALOG_WALK_BUDGET_SECONDS
hit_deadline = False

for _ in range(max_pages):
if time.monotonic() > deadline:
hit_deadline = True
break
Comment thread tools/skills_hub.py
Comment on lines +2235 to +2239
# Only cache a walk that reached a natural stop (cursor exhausted or
# page cap). A walk truncated by the wall-clock budget is partial, so
# writing it would poison the cache with incomplete catalog data.
if not hit_deadline:
_write_index_cache(cache_key, [_skill_meta_to_dict(s) for s in results])
Comment thread tools/skills_hub.py
Comment on lines +3743 to +3746
finally:
# wait=False so a slow source cannot block the caller's return;
# cancel_futures drops not-yet-started work.
pool.shutdown(wait=False, cancel_futures=True)
Comment on lines +2254 to +2255
fast = _FakeSource("fast", sleep=0.0, results=[self._meta("fast")])
slow = _FakeSource("slow", sleep=5.0, results=[self._meta("slow")])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Medium — degraded but workaround exists tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills browse hangs indefinitely on cold cache: overall_timeout defeated by blocking ThreadPoolExecutor shutdown + unbounded ClawHub catalog walk

3 participants