Skip to content

feat(search): list an organization's open searches and report its search limits#327

Draft
tomaz-lc wants to merge 1 commit into
masterfrom
feat-list-active-search-queries
Draft

feat(search): list an organization's open searches and report its search limits#327
tomaz-lc wants to merge 1 commit into
masterfrom
feat-list-active-search-queries

Conversation

@tomaz-lc

@tomaz-lc tomaz-lc commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Details

Adds SDK and CLI support for two search API endpoints that answer questions the SDK previously could not answer without guessing: what an organization currently has open, and what its search limits actually are.

Search.list_open_queries() / Search.iter_open_queries() and limacharlie search queries report the searches an organization has open and which of them consume its concurrency limit. Those are different populations, and conflating them is the mistake this exists to prevent: a paginated search sitting between pages is open and resumable but holds no slot. The response reports slotsHeld separately from count, and slotsHeld is what limit applies to. Each entry carries the query and range as submitted, who submitted it and from which client, how long the current page has been running, progress, how much has been scanned and billed, and the two unrelated expiries. This is what to reach for after a refusal for concurrency: it names what is holding the slots, so the search worth cancelling is identifiable.

Search.get_limits() and limacharlie search limits report the limits themselves: how many searches may run at once, the page shape, how long results stay resumable, how long a produced page is retained, and any enforced execution deadlines. Every one of these was previously discoverable only by hitting it - a refusal for concurrency does not say what the cap was, and a paginated search stops being resumable with no way to have known the window.

Conventions the SDK preserves rather than smooths over

A limit that is not enforced comes back as None, never 0. In a set of limits a 0 would read as "nothing allowed", which is the opposite of "no limit applies". The SDK does not coerce one into the other, and a test pins that.

Fields are additive. Unknown ones are ignored rather than rejected, so an older SDK keeps working against a newer deployment, and an absent field means "not applicable to this deployment" rather than zero.

capabilities.openQueryListing says whether the listing can report searches that are open but idle. A caller uses it to decide what to offer, instead of probing the listing and interpreting an empty result - which is ambiguous between "not available here" and "nothing is open".

Paging

iter_open_queries() is driven by the response's truncated flag rather than by how many rows came back, because the state filter is applied after paging: a page can legitimately be empty while more entries exist behind it. Entries are de-duplicated by id, since a search leaving the listing mid-walk shifts later offsets back and can re-serve one already returned.

The queries command walks every page by default so its output is a complete answer rather than one server page; pass --limit for a single page.

Blast radius / isolation

Purely additive. Two new SDK methods on Search, one new iterator, two new CLI subcommands. No existing method, command, or output format changes. Nothing here runs unless called.

Both endpoints are gated server-side and may be unavailable on a given deployment; the listing degrades to reporting only searches holding a slot, and callers should treat an absent limits endpoint as "not available" rather than as a failure.

Performance characteristics

One request per call. iter_open_queries() issues one request per page (server page size clamps to 200), not one per row. get_limits() resolves server-side configuration only and touches no datastore, so the result is stable between calls and safe for a caller to cache for the lifetime of a process.

Notable contracts / APIs

No wire-format or schema changes originate here - this consumes existing API responses. The None-versus-0 distinction on unenforced limits and the ignore-unknown-fields behaviour are the two parts of the response contract a caller must not paper over.

Tests

11 new unit tests. For the listing: request shape and that the organization is named in the path rather than as a parameter, filter and paging pass-through, a bad state rejected client-side before it reaches the server, transport failures wrapped with region and organization context, and the paging walk continuing past an empty page while truncated is set, de-duplicating a row re-served by a shifting offset, and stopping on error.

For the limits: request shape with no query parameters (the endpoint takes none, and sending some would be silently ignored rather than erroring), the payload returned unaltered, None preserved for unenforced limits, transport failures wrapped, and an already-typed SearchError from deeper in the stack re-raised rather than re-wrapped, which would otherwise bury the original message.

The limits command is registered in the CLI lazy-loading regression test, which is what keeps a new subcommand from silently importing the client at startup.

Related PRs

🤖 Generated with Claude Code

…rch limits

Two additions that answer questions the SDK could not previously answer
without guessing.

Search.list_open_queries() / iter_open_queries(), and
`limacharlie search queries`, report the searches an organization currently
has open and which of them consume its concurrency limit. Those are
different populations: a paginated search sitting between pages is open and
resumable but holds no slot, so the response reports slotsHeld separately
from count, and slotsHeld is what the limit applies to. Each entry carries
the query and range as submitted, who submitted it and from which client,
how long the current page has been running, progress, how much has been
scanned and billed, and the two unrelated expiries. This is what to reach
for after a refusal for concurrency: it names what is holding the slots, so
the search worth cancelling is identifiable.

Search.get_limits(), and `limacharlie search limits`, report the resolved
limits themselves: how many searches may run at once, the page shape, how
long results stay resumable, how long a produced page is retained, and any
enforced execution deadlines. Every one of these was previously
discoverable only by hitting it.

A limit that is not enforced comes back as None, never 0 - in a set of
limits a 0 would read as "nothing allowed", which is the opposite - and the
SDK preserves that distinction rather than coercing it. Fields are
additive, so unknown ones are ignored rather than rejected.

capabilities.openQueryListing says whether the listing can report searches
that are open but idle, so a caller can decide what to offer instead of
probing and interpreting an empty result, which is ambiguous between "not
available" and "nothing open".

The walk in iter_open_queries() is driven by the response's truncated flag
rather than by how many rows came back, because the state filter is applied
after paging and a page can legitimately be empty while more entries exist.
Entries are de-duplicated by id, since a search leaving the listing
mid-walk shifts later offsets back and can re-serve one already returned.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant