Repository navigation
fix(cascade): embed a file's changed entries in one batched call - #489
Merged
Merged
Conversation
The four daily-log handlers each called the embedding endpoint once per entry: _embed_entries gathered _build_row over the changed entries and every _build_row awaited embed_or_none. embed_batch, which chunks by [embedding].batch_size, was only ever used by the vector backfill, so batch_size had no effect on the live write path. One turn that writes ~20 atomic facts therefore queued ~20 single-input requests on the process-wide embedding semaphore, and the next search's query embedding waited behind all of them -- about five times the endpoint's latency at the previous max_concurrent of 5. Measured against a stub endpoint of 1 s per request, 23 entries: 24 requests and 5.13 s of query wait before, 4 requests and 1.05 s after. Subclasses now declare the texts they embed in _embed_texts; the base sends every text of a file in one embed_batch call and hands each entry its vectors. _build_row became a pure mapping. The provider now rejects a response whose vector count differs from the input count: callers slice the batch back per input, so a short list would silently shift every later vector onto the wrong text. [embedding].max_concurrent now defaults to 10, and the EmbeddingSettings class default (a dead 50, always overridden by the shipped TOML) is aligned to it. Refs: #486 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kendrick-Song
approved these changes
Oct 13, 2026
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
Fixes #486. Every daily-log cascade handler embedded one entry per HTTP request.
BaseDailyLogHandler._embed_entriesgathered_build_rowover the changedentries, and each
_build_rowawaitedembed_or_none->provider.embed(text)->
_embed_chunk([text]).embed_batch, which chunks by[embedding].batch_size, was reachable only from the vector backfill, sobatch_sizehad no effect on the live write path.Because the provider (and its semaphore) is a process-wide singleton, a turn
that writes ~20 atomic facts put ~20 single-input requests in front of the next
search's query embedding. The query then waited roughly
ceil(N / max_concurrent)endpoint round trips instead of one.
Subclasses now declare what they embed in a new
_embed_texts(entry); the baseflattens one file's texts into a single
embed_batchcall and hands each entryits vectors, so
_build_rowbecame a pure field mapping. Two adjacent changescame out of review:
OpenAIEmbeddingProvider._embed_chunknow rejects a response whose vectorcount differs from the input count. Callers slice a batch back per input, so a
short list would silently shift every later vector onto the wrong text, and
the row's
content_sha256already matches, so a later sync would not re-embedit.
[embedding].max_concurrentnow defaults to 10, and theEmbeddingSettingsclass default (a dead 50, always overridden by the shipped TOML) is aligned to
the same value.
Not addressed here: #473 (query embedding shares the cascade's semaphore). The
two fixes are complementary -- batching shortens the queue, a separate lane
keeps the query out of it.
Area
Verification
make cipasses on this branch (exit 0): lint + import-linter contracts +2678 unit passed / 4 skipped + 186 integration passed / 7 skipped.
Measured end to end through the real
EpisodeHandler.handle_added_or_modifiedand the shipped
OpenAIEmbeddingProvider, pointed at a local stub/v1/embeddingsof fixed 1 s latency, with 23 entries in one file atmax_concurrent=5,batch_size=10. A query embedding is issued concurrentlywhile the cascade pass runs:
An independent reviewer reproduced this against a baseline rebuilt from
origin/mainand extended it: with a latency model calibrated to the DeepInframeasurement in the issue (0.3 s + 0.07 s per input), the query wait goes
2.22 s -> 0.38 s; across a 50-file worker drain, 54.35 s -> 11.25 s.
The three new tests were each verified to fail against a mutated source (per-text
embedloop;embed_batch_or_nonereturning[]when unavailable; reversedbatch order) and to pass after restoring.
Checklist
main..envfiles, dependency folders, or generated output.Notes for Reviewers
The contract change.
BaseDailyLogHandlergained an abstract_embed_texts, and_build_rowgained avectorskeyword. All four subclassesare in this diff; no other subclass or caller exists in
src,tests, or thesibling repos.
agent_skill/knowledge_topicstill useembed_or_noneonpurpose: they are single-file kinds with one text each, so there is no fan-out
to batch.
Failure scope is unchanged. Before and after, an embedding error leaves
_embed_entriesbefore any LanceDB write, so the whole file fails and nothingis written. The per-row fallback that
_backfill.pyuses for poison rows wasdeliberately not copied to the live path; a 400 now takes down a chunk of up to
batch_sizerather than one request, which is invisible at file granularity.One request now carries up to
batch_sizetexts whiletimeout_secondsstill bounds one request. On a slow serialized endpoint (CPU Ollama, a single
GPU) queued batch requests can exceed the 30 s default where single-input
requests did not.
docs/configuration.mdnow says to lowerbatch_sizeandmax_concurrenttogether, or raisetimeout_seconds, for that deploymentshape. Scaling the timeout with chunk length would be the thorough fix and is
left out of this PR.
On
max_concurrent5 -> 10. It is not what fixes the reported case: withbatching, single-file query wait measures the same at 5 and at 10. It halves the
wait only for a backlog on an endpoint that can serve 10 requests in parallel.
Reviewers who favour keeping 5 should say so -- the batching change stands on
its own.
🤖 Generated with Claude Code