PageIndex/pageindex
mountain 2d46d68052 fix(index): bound LLM concurrency at the leaf, not per gather call
The previous approach (bounded_gather building a fresh semaphore per call)
did NOT compose: the indexing call graph nests gathers
(tree_parser -> process_large_node_recursively -> recurse, plus each node's
check_title gather), and each level got its own independent cap. Peak
in-flight LLM calls grew ~N^depth, so a deep/wide document still exhausted
file descriptors (Errno 24) — the exact failure the cap was meant to
prevent — while the flat summary phase was over-serialized. Naively sharing
one semaphore across gather levels would instead deadlock (a parent holds a
slot while awaiting children that need slots).

Move the throttle to the single chokepoint every LLM call funnels through,
llm_acompletion: one shared semaphore per event loop, acquired only around
the litellm.acompletion network call. This gives a true global cap that
composes across any nesting and can't deadlock (a parent awaiting children
holds no slot). bounded_gather is gone; the call sites revert to plain
asyncio.gather.

Also:
- Reject bool in max_concurrency validation (bool is an int subclass, so
  set_max_concurrency(True) / IndexConfig(max_concurrency=True) previously
  became Semaphore(1) and silently serialized). Shared _validate_max_concurrency
  + a pydantic field_validator.
- Guard check_title_appearance_in_start_concurrent against an out-of-range
  or 0 physical_index (LLM can emit one): it was dereferenced during task
  construction, outside the gather's return_exceptions protection, aborting
  the whole build; 0 silently wrapped to the last page. Now marked 'no'.
- Propagate contextvars into agent.py's worker-thread run (mirrors
  pipeline._run_async) so ContextVar settings stay consistent.
- Tests rewritten to cover the nested case the old flat tests missed, the
  leaf-level throttle in llm_acompletion, bool rejection, and the
  out-of-range physical_index guard.

Claude-Session: https://claude.ai/code/session_01Kx5DgKbhK1N8autqXH8SmS
2026-07-08 11:26:36 +08:00
..
backend style(cloud): annotate best-effort response-close in query_stream cleanup 2026-07-07 16:02:33 +08:00
index fix(index): bound LLM concurrency at the leaf, not per gather call 2026-07-08 11:26:36 +08:00
parser refactor(sdk): typed returns, protocol contract, parser layering 2026-07-07 12:15:34 +08:00
storage fix(sqlite): make concurrent indexing writes robust (no "database is locked") 2026-07-07 18:23:01 +08:00
__init__.py fix: load .env explicitly in pageindex __init__ 2026-07-07 17:00:34 +08:00
agent.py fix(index): bound LLM concurrency at the leaf, not per gather call 2026-07-08 11:26:36 +08:00
client.py refactor(sdk): typed returns, protocol contract, parser layering 2026-07-07 12:15:34 +08:00
cloud_api.py fix(cloud_api): restore legacy is_retrieval_ready swallow-on-error contract 2026-07-07 15:48:49 +08:00
collection.py refactor(sdk): typed returns, protocol contract, parser layering 2026-07-07 12:15:34 +08:00
config.py fix(index): bound LLM concurrency at the leaf, not per gather call 2026-07-08 11:26:36 +08:00
errors.py fix(cloud): align cloud/local backend contracts and harden the HTTP layer 2026-07-07 09:40:09 +08:00
events.py feat: add PageIndex SDK with local/cloud dual-mode support (#207) 2026-04-08 20:21:58 +08:00
page_index.py refactor(index): dedupe the copied indexing pipeline behind deprecation shims 2026-07-07 10:42:23 +08:00
page_index_md.py refactor(index): dedupe the copied indexing pipeline behind deprecation shims 2026-07-07 10:42:23 +08:00
retrieve.py refactor(index): dedupe the copied indexing pipeline behind deprecation shims 2026-07-07 10:42:23 +08:00
tokens.py refactor(sdk): typed returns, protocol contract, parser layering 2026-07-07 12:15:34 +08:00
types.py refactor(sdk): typed returns, protocol contract, parser layering 2026-07-07 12:15:34 +08:00
utils.py refactor(index): dedupe the copied indexing pipeline behind deprecation shims 2026-07-07 10:42:23 +08:00