Conversation
tomaz-lc
marked this pull request as ready for review
September 25, 2026 05:51
tomaz-lc
force-pushed
the
feat-search-mode
branch
from
September 25, 2026 06:08
06bda8b to
f5588e3
Compare
LimaCharlie Cloud Security — code scanNo new code findings were introduced by this pull request. This check reports and never fails: no Scanned This comment is updated in place on every push to this pull request. |
Contributor
Author
|
/lc-review |
tomaz-lc
force-pushed
the
feat-search-mode
branch
from
September 25, 2026 15:02
f5588e3 to
379a8c8
Compare
…active in the CLI POST /v1/search accepts an optional "mode" declaring how the caller intends to consume the search rather than how much data it wants: "interactive" favours time to first results, "batch" favours throughput over the whole result set, which means fewer and larger pages. The rows and their ordering are identical either way; only where the page boundaries fall changes. Search.execute() defaults it to "batch". This is a behavior change, not a backward-compatible addition: an existing SDK caller that does not pass mode now sends a body with a key it did not have and gets different page shapes. Nobody reads different data, but a caller that sizes buffers per page, meters progress per page or has timeouts tuned to the old shape will notice. The default is batch because the callers here are scripts and automation that read every page and pay a fixed cost per round trip, and the shape tuned for a human watching a screen is the wrong one for them. Passing mode=None explicitly restores the previous request exactly and leaves the choice to the server; None stays meaningful, it is just no longer the default. The CLI resolves its own mode on every path rather than inheriting that, so the SDK default can never reach a command line. It defaults to interactive, and to batch only where the run is evidently a bulk retrieval: driven by --checkpoint or --resume, stdout redirected to a file or a pipe, or --output jsonl, csv or toon, the three formats that exist to be consumed by something other than a reader. Everything the signals leave ambiguous, --output json at a terminal included, stays interactive: an over-large page in front of someone waiting is worse than a chattier fetch nobody is watching. --mode overrides the default in every case. An unrecognised spelling is refused client-side, with ValidationError in the SDK and click.Choice in the CLI, matching how an unknown "state" is handled on list_open_queries. The server ignores a value it does not recognise and runs the search as interactive, so a typo would otherwise spend the query and produce a mode the caller did not ask for with nothing to signal it. Matching is exact, so "Batch" is a typo like any other. The isinstance guard in front of the set lookup keeps an unhashable value from raising TypeError in place of the refusal. The mode is submitted once. Continuation pages are GETs carrying only the pagination token, so they inherit it and never resend it. A checkpointed run records the mode it resolved to, and a resume, which opens a fresh search, repeats that record; --mode on the resume overrides it for that leg, which is safe because the mode moves the page boundaries and nothing else. Metadata predating the field carries no mode and falls back to the checkpoint-driven default. Each page reports what it actually ran as in its stats: searchMode, pageSize and paginatedByteCap, all three absent for a search that ran without pagination. Those reach --output json and jsonl untouched, and the stderr stats line leads with the applied mode, since the requested one is a hint that the server, which enables the mode per organization and may select one itself, need not take. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… by default Five tests that submit real searches and assert on what the server reports back, rather than on what was requested. The mode is served by the search backend, so until that support is deployed these fail for a server-side reason with nothing wrong in this package. They are gated twice over so that cannot turn a build red. tests/integration is already outside testpaths, so the default pytest run that CI executes never collects the file. On top of that every test skips unless LC_TEST_SEARCH_MODE_E2E is set, which is the opt-in that says the backend being pointed at is expected to serve the field. The skip reason names the variable, the --oid and --key requirement, and the deployment dependency, so a future reader hitting a skip does not have to guess. A searchmode_e2e marker, registered in pyproject.toml, lets an otherwise live integration run drop them with -m "not searchmode_e2e". Coverage is deliberately small: a paginated search submitted with each mode reports an applied searchMode; a paginated page carries all three of searchMode, pageSize and paginatedByteCap; the applied mode is identical across every page of one search, which is the part of the contract this package leans on by never resending the mode; rows arrive whole, so a page boundary never falls through a record; and the two modes return the same rows in the same order. None of them asserts that searchMode equals the requested mode. The mode is a hint the server may decline, and a test demanding equality would fail for a legitimate reason and teach whoever sees it to ignore the file. They assert the field is present and is a mode this package knows. Conditions that are about the environment rather than the code, an organization with too little telemetry to paginate or a window that moved between two runs, skip with a reason instead of failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he CLI The server now serves the search mode field, so the end-to-end tests no longer wait on a deployment. The PR build's integration step sets LC_TEST_SEARCH_MODE_E2E=1 next to its --oid and --key, so they run there. They still skip by default elsewhere, because they start real, billed searches and need an organization with telemetry in the last day. Three more paths are now covered against a live server: - mode=None sends no mode key and a page still reports what it ran as; - the CLI's own resolution (JSONL output, stdout not a terminal) reaches a real submission and the page reports its applied mode; - --mode interactive overrides that resolution on a real submission. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tomaz-lc
force-pushed
the
feat-search-mode
branch
from
September 25, 2026 16:59
379a8c8 to
c3ee70c
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
POST /v1/searchaccepts an optionalmodefield declaring how the caller intends to consume a search, and each paginated page reports what it actually ran as. This binds both sides of that in the SDK and the CLI.modeis"interactive"(optimize for time to first results) or"batch"(optimize for throughput over the whole result set, which means fewer and larger pages). It says how the results will be read, not how many of them are wanted: the caller never sends a row count, the server sizes the pages. Results and their ordering are identical in both modes. Only where the page boundaries fall changes.Three things below deserve a reviewer's attention before the rest: the SDK default is a behavior change, the CLI deliberately does not inherit it, and invalid values are refused client-side at a stated cost.
Read this first: the SDK default is a behavior change
Search.execute()defaults tomode="batch". This is a behavior change, not a backward-compatible addition.Every existing SDK caller that does not pass
modenow sends a request body with a key it did not send before, and gets different page shapes: fewer and larger pages.mode=Noneis the exact restoration. Passing it explicitly sends nomodekey at all, producing a byte-identical request to the one built before the field existed, and leaves the choice to the server.Noneremains meaningful; it is simply no longer the default.The default is
batchbecause the callers of this method are scripts and automation. They read every page and pay a fixed cost per round trip, so the throughput shape is the right one for them, and the shape tuned for a human watching a screen is the wrong one. A client that puts a person in front of the results passes"interactive"for itself rather than relying on this default, which is exactly what the CLI in this repo does.Read this second: the CLI does not inherit that default
A person running a search at a terminal wants the first rows quickly. So the CLI resolves its own mode on every execution path and the SDK default can never reach a command line.
The rule: default to
interactive, and switch tobatchonly where the run is evidently a bulk retrieval. A run qualifies when any of these hold:--checkpoint, with or without--resume), whose whole purpose is to fetch a result set to a file, resumably;jsonl,csvortoon.Everything else resolves to
interactive, including every case those signals leave ambiguous. The asymmetry is deliberate: an over-large page in front of someone waiting at a terminal is a worse failure than a slightly chattier bulk fetch that nobody is watching.Clause 3 follows this repo's own statements about each format rather than a guess.
jsonlis whatsearch run --explainalready recommends for 100K+ events,csvis an export (sensor export --output csv), andformat_toon's docstring says TOON exists for feeding CLI output into LLM prompts. None of the three is read a page at a time.jsonandyamlare excluded on purpose: both are readable, andjsonis merely what an unset--outputresolves to whenever stdout is not a terminal, which clause 2 already covers.Per-command outcome
search runat a terminalinteractivesearch runredirected or pipedbatchsearch run --output jsonl/csv/toonbatchsearch run --checkpointbatchsearch run --resumebatchsearch saved-run--checkpointsearch validate,estimate,queries,limits,saved-list/get/create/delete,checkpoints,checkpoint-show--modeoverrides the default in every one of these cases.--output jsonat a terminal is the one genuinely ambiguous case, and it staysinteractiveper the rule above.Read this third: invalid values are refused client-side
Anything that is not exactly
"interactive"or"batch"is refused before the request is sent:ValidationErrorin the SDK,click.Choicein the CLI. Matching is exact, so"Batch"is a typo like any other, as is a non-string value.The reason is that the server ignores a value it does not recognise and runs the search as
interactive. Sending a typo therefore costs a full query, returns the mode the caller did not ask for, and reports nothing wrong. That is the hardest kind of failure to notice, and it is worth an up-front refusal. It also matches how this class already treats an unknownstateonlist_open_queries, and how--stateis aclick.Choice.The honest tradeoff: a mode added server-side in future needs an SDK bump before callers of this package can use it. That is a real cost. It is accepted here because the set is small and stable, and because a silent no-op is the worse outcome of the two.
Implementation details
modeis submitted once, on thePOST. Continuation pages areGETs carrying only the pagination token and no body at all, so they inherit the mode and can never resend it. It is not part of the token.The mode is a hint and never an assumption. It is enabled per organization and the server may also select the mode itself, so a search can run as something other than what was asked for. Every page therefore reports what it actually ran as, in its result
stats:searchModepageSizepaginatedByteCapAll three are omitted for a search that ran without pagination.
pageSizeis a target rather than a promise: a page can end below it on a time limit, a byte limit or the end of the data, and slightly above it, because a page stops only once a whole batch of results has arrived.The SDK already yielded the server's
SearchResultobjects untouched, so these three fields reach callers and--output json/jsonlwith no parsing code added. What was missing was the human-readable path:--mode batchwith no feedback about whether it was honoured. The stderr stats line now leads with the applied mode, read from the page's own stats rather than fromcumulativeStats, since the counters roll up across pages but the mode is a property of this one page.modeapplies to a paginated search only. A query that must process all the data before it can answer anything, such asGROUP BY,ORDER BYor an aggregation over every record, is unaffected.Two things a reviewer should know rather than discover
--resumeopens a fresh search. It does not continue the original one: it issues a newPOSTand hands the stored token to the firstGET. A resume is therefore a submission and has to re-state the mode, or a batch search silently downgrades to interactive halfway through. A checkpointed run records the mode it resolved to, and a resume repeats that record.--modeon a resume overrides it for that leg, which is allowed unlike--queryor--start, because the mode moves the page boundaries and nothing else: the rows already on disk and the rows still to come are the same either way. Checkpoint metadata written before this change has nomodekey, reads back as absent, and falls back to the checkpoint-driven default.search run --quietsubmits no search at all._stream_search_outputreturns early onctx.obj.quietwithout ever iterating the results generator. This is existing behavior onmasterand is not introduced or changed here. It came up while deciding whether--quietneeded a rule of its own; it does not, because there is no submission. A test records it so a future change to--quietsurfaces there rather than as a silently batch-shaped run.Backward compatibility
Search.execute()defaultmode=Nonerestores the previous request exactly.execute()signaturemodeis keyword-accessible and every other parameter is unchanged and in the same position.search run/saved-run--modestill sends the mode best suited to how that invocation is being consumed, and output is byte-identical.POSTbody. A server that does not support the field ignores it, and an unrecognised value is treated asinteractive, so sending it cannot fail a request.modekey is additive and every reader uses.get(). Old metadata read by new code falls back to the default; new metadata read by old code ignores the key. No schema version bump needed.Feature flags
None. This package has no feature-flag mechanism; the behavior is selected per call by
modeand per invocation by--mode.Performance characteristics
No measurable change in this package.
modeadds one short optional key to one request body per search, and the resolver is a handful of comparisons run once per invocation. Nothing is added to any per-page or per-row path, and no allocation behavior changes.What does change is the shape of the work the server returns:
batchmeans fewer, larger pages for the same result set, so fewer round trips and correspondingly larger responses per trip. That is the point of the field. This PR carries no throughput figures, because the effect is produced server-side and measuring it here would report the server's behavior rather than this package's.Testing
pytest tests/unit/ tests/microbenchmarks/, the invocation CI runs: 4691 passed, 5 skipped on this branch rebased onto the currentmaster, against 4572 passed onmasterwith the same command, so this adds 119 tests and changes no existing one.Coverage is split across three files:
tests/unit/test_sdk_search.py- the request body, byte for byte: the default submitsbatch, an explicitNonesubmits a body byte-identical to one built before the field existed, and the default is pinned on the signature viainspectas well as through a call, because the signature is where a reader forms the wrong assumption. Plus exact-match rejection over cased and non-string values with no request made, onePOSTacross a three-page search with token-onlyGETs, the token-resume submission, and the three stats fields present, absent, and differing from what was requested.tests/unit/test_cli_search_mode.py- the rule end to end throughCliRunnerwith only the HTTP layer mocked: every format on both sides of the terminal check, redirected output, checkpoint runs, all four resume precedence cases,saved-run, and--modeoverriding on all six formats. A guard test asserts that the bulk-format set plus the explicitly listed readable set exactly equals--output's own choices, so a format added to one list and not the other fails loudly instead of being classified by omission.tests/unit/test_search_helpers.py- the stats line: the applied mode leads it, is read from the page stats and not the roll-up, is omitted when the search did not paginate, survives an unrecognised value, and is escaped for control characters since it arrives over the wire.Live end-to-end tests, run in CI
tests/integration/test_search_mode_e2e.pyholds eight tests that submit real searches and assert on what the server reports. The PR build's integration step setsLC_TEST_SEARCH_MODE_E2E=1next to its--oid/--key, so they run there, and all eight pass (the step: 171 passed, 2 skipped elsewhere in the suite).Everywhere else they skip by default, gated twice over, because they start real, billed searches and need an organization with telemetry in the last day:
tests/integrationis outsidetestpathsin pyproject.toml, so a plainpytestrun never collects the file.LC_TEST_SEARCH_MODE_E2Eis set. Running the file with credentials but without the variable yields8 skipped.To run them:
They also carry a
searchmode_e2emarker, registered in pyproject.toml, so an otherwise live integration run can drop them with-m "not searchmode_e2e".test_a_submitted_mode_comes_back_as_an_applied_mode(batch, interactive)searchModetest_a_paginated_page_reports_all_three_shape_fieldssearchMode,pageSizeandpaginatedByteCaptest_the_applied_mode_holds_for_every_page_of_one_searchtest_both_modes_return_the_same_rows_in_the_same_ordertest_no_mode_leaves_it_to_the_organizationmode=Nonesends no key and a page still reports a modetest_the_cli_bulk_default_reaches_the_server_and_comes_back_appliedsearch run --output jsonlwith stdout not a terminal resolves its own mode and the page reports itbatch, ran asbatchtest_the_cli_mode_flag_reaches_the_server_and_comes_back_applied--mode interactiveoverrides that resolution on a real submissioninteractiveThey never assert that
searchModeequals what was requested. The mode is a hint that the server may decline, so demanding equality would fail for a legitimate reason and teach whoever sees it to ignore the file. They assert the field is present and is a mode this package knows; the applied modes in the table are what the test organization reported, printed by the tests. Where the environment is not rich enough to answer the question, for example an organization with too little telemetry to paginate, they skip with a reason saying so rather than failing on a condition that is not about this code.Three checks were run against the tests themselves rather than the code. Reverting the SDK default to
Nonefails 2 tests; making the CLI blindly inheritbatchfails 8; and becauseCliRunnernever provides a terminal, the resolver was additionally exercised under a real pty and a real pipe, which reproduced the per-command table above exactly.Rollback
Ordered least disruptive first.
mode=None. A CLI user who disagrees with a default passes--mode.limacharlie/sdk/search.py,limacharlie/commands/search.pyandlimacharlie/search_checkpoint.py, with no migration and no persisted state to unwind: themodekey left in existing checkpoint metadata is ignored by the reverted code.What to watch after release
mode=Noneormode="interactive"is the answer.searchModein page stats that consistently disagrees with what was requested. That is the server declining the hint, which is legitimate, but a persistent mismatch is worth knowing about.ValidationErroronmodefrom callers, which would mean the accepted set has fallen behind the server's.Links and references
doc/sdk/search-insight.mdanddoc/cli/data-query.mdin this PR document the field for SDK and CLI users respectively.search run --explaincarries the full rule and the per-command reasoning for CLI users.Related PRs
None. This change is contained to this repository.
🤖 Generated with Claude Code