Skip to content
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@
## 2026-07-09 - Avoid N+1 API blocking in SBOM aggregator
**Learning:** The `collect_inventories` function in `scripts/ci/sbom_inventory_aggregator.py` was fetching SBOMs from the GitHub dependency graph synchronously for every repository in the organization. For large organizations (up to 500 repos), this N+1 network/CLI bottleneck significantly stalled the aggregation workflow.
**Action:** Use `concurrent.futures.ThreadPoolExecutor` to fetch SBOMs concurrently when multiple repositories are provided, bounded by a `max_workers` limit (e.g., 10) to avoid overwhelming the CLI/API, while preserving the fast serial path for single-item inputs.
## 2026-08-10 - Avoid N+1 API blocking in agent mention sweep
**Learning:** In `scripts/ci/agent_mention_sweep.py`, synchronously fetching recent pull requests for every active repository using `list_recent_pull_requests` causes an N+1 API bottleneck. For organizations with many repositories, this delays sweep operations.
**Action:** Use `concurrent.futures.ThreadPoolExecutor` with bounded `max_workers` to fetch multiple repositories concurrently, significantly speeding up the collection of candidate pull requests.
165 changes: 108 additions & 57 deletions scripts/ci/agent_mention_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from __future__ import annotations

import argparse
import concurrent.futures
import os
import re
import threading
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Callable, Iterator, Sequence
Expand Down Expand Up @@ -78,9 +80,7 @@ def flatten_pages(
if not isinstance(collection, list):
raise ValueError("paginated GitHub response is not a list")
if not all(isinstance(record, dict) for record in collection):
raise ValueError(
"paginated GitHub response contains a non-object record"
)
raise ValueError("paginated GitHub response contains a non-object record")
records.extend(collection)
return records

Expand Down Expand Up @@ -155,10 +155,24 @@ def list_recent_pull_requests(
organization=organization,
repository_source=repository_source,
)
for repository in repositories:

def fetch_repo_pulls(
repository: str,
stop_event: threading.Event | None = None,
) -> tuple[list[dict[str, Any]], Exception | None]:
"""Return one repository's bounded PR candidates and isolated failure.

Collection stops before the next page when ``stop_event`` is set or
when the first item older than ``cutoff`` is reached. A non-positive
or non-integer pull-request number is returned as a ``ValueError`` for
serialized handling by the caller.
"""
repo_pulls: list[dict[str, Any]] = []
try:
page = 1
while True:
if stop_event is not None and stop_event.is_set():
break
response = client.request(
[
f"repos/{repository}/pulls",
Expand All @@ -182,9 +196,7 @@ def list_recent_pull_requests(
reached_cutoff = False
for pull_request in pull_requests:
if (
parse_timestamp(
str(pull_request.get("updated_at") or "")
)
parse_timestamp(str(pull_request.get("updated_at") or ""))
< cutoff
):
reached_cutoff = True
Expand All @@ -194,23 +206,60 @@ def list_recent_pull_requests(
raise ValueError(
"GitHub returned an invalid pull request number"
)
yield {
"number": number,
"repository": repository,
"pull_request": {
"url": (
"https://api.github.com/repos/"
f"{repository}/pulls/{number}"
)
},
}
repo_pulls.append(
{
"number": number,
"repository": repository,
"pull_request": {
"url": (
"https://api.github.com/repos/"
f"{repository}/pulls/{number}"
)
},
}
)
if reached_cutoff or len(pull_requests) < 100:
break
page += 1
except Exception as exc: # noqa: BLE001 - repository isolation boundary
return repo_pulls, exc
return repo_pulls, None

def emit_repo_result(
repository: str,
result: tuple[list[dict[str, Any]], Exception | None],
) -> Iterator[dict[str, Any]]:
"""Yield worker results and report failures serially on the caller thread."""
repo_pulls, error = result
if error is not None:
if on_error is None:
raise
on_error(repository, exc)
raise error
on_error(repository, error)
yield from repo_pulls

if len(repositories) <= 1:
for repository in repositories:
yield from emit_repo_result(repository, fetch_repo_pulls(repository))
else:
max_workers = min(10, len(repositories))
stop_event = threading.Event()
executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
futures = [
executor.submit(fetch_repo_pulls, repository, stop_event)
for repository in repositories
]
completed = False
try:
for repository, future in zip(repositories, futures, strict=True):
result = future.result()
yield from emit_repo_result(repository, result)
completed = True
finally:
if completed:
executor.shutdown(wait=True)
else:
stop_event.set()
executor.shutdown(wait=False, cancel_futures=True)


def list_recent_comments(
Expand Down Expand Up @@ -305,50 +354,54 @@ def record_failure(scope: str, error: Exception) -> None:

counters.failures += 1
message = " ".join(str(error).split()) or error.__class__.__name__
print(
f"::warning::Agent mention sweep skipped {scope}: {message[:1000]}"
)
print(f"::warning::Agent mention sweep skipped {scope}: {message[:1000]}")

for issue in list_recent_pull_requests(
issues = list_recent_pull_requests(
target_client,
organization=organization,
repository_source=repository_source,
since=since,
on_error=record_failure,
):
issue_scope = f"{issue.get('repository')}#{issue.get('number')}"
try:
requests = build_requests_for_pull_request(
target_client,
issue=issue,
since=since,
)
except Exception as exc: # noqa: BLE001 - pull-request isolation boundary
record_failure(issue_scope, exc)
continue
for request in requests:
request_scope = f"{issue_scope}/comment-{request.comment_id}"
)
try:
for issue in issues:
issue_scope = f"{issue.get('repository')}#{issue.get('number')}"
try:
queued_agents = dispatch_request(
request,
target_client=target_client,
dispatch_client=dispatch_client,
opencode_allowlist=opencode_allowlist,
dry_run=dry_run,
ledger_artifact_cache=ledger_artifact_cache,
requests = build_requests_for_pull_request(
target_client,
issue=issue,
since=since,
)
except Exception as exc: # noqa: BLE001 - request isolation boundary
record_failure(request_scope, exc)
except Exception as exc: # noqa: BLE001 - pull-request isolation boundary
record_failure(issue_scope, exc)
continue
if not queued_agents:
continue
dispatched += 1
if dispatched >= max_dispatches:
print(
"Agent mention sweep reached dispatch limit "
f"{max_dispatches}; isolated failures={counters.failures}."
)
return dispatched
for request in requests:
request_scope = f"{issue_scope}/comment-{request.comment_id}"
try:
queued_agents = dispatch_request(
request,
target_client=target_client,
dispatch_client=dispatch_client,
opencode_allowlist=opencode_allowlist,
dry_run=dry_run,
ledger_artifact_cache=ledger_artifact_cache,
)
except Exception as exc: # noqa: BLE001 - request isolation boundary
record_failure(request_scope, exc)
continue
if not queued_agents:
continue
dispatched += 1
if dispatched >= max_dispatches:
print(
"Agent mention sweep reached dispatch limit "
f"{max_dispatches}; isolated failures={counters.failures}."
)
return dispatched
finally:
close = getattr(issues, "close", None)
if close is not None:
close()
print(
"Agent mention sweep completed with "
f"{dispatched} dispatch(es) and {counters.failures} isolated failure(s)."
Expand All @@ -375,9 +428,7 @@ def main(argv: Sequence[str] | None = None) -> int:
)
metrics = SweepMetrics()
sweep(
target_client=GitHubClient(
os.environ.get("TARGET_REPOSITORY_TOKEN", "")
),
target_client=GitHubClient(os.environ.get("TARGET_REPOSITORY_TOKEN", "")),
dispatch_client=GitHubClient(os.environ.get("AGENT_DISPATCH_TOKEN", "")),
organization=args.organization,
repository_source=args.repository_source,
Expand Down
Loading
Loading