Repository navigation
fix(retain): report processed content tokens for oversized re-retains - #5365
Merged
Merged
Conversation
An item over `retain_batch_tokens` is split into sub-batches, and only the first may take the delta path. The later sub-batches run the streaming path, find their chunks already committed and skip them, but the streaming path returned `processed_content_tokens=None` unconditionally. None is contagious across sub-batches, so a re-retain of a large, mostly unchanged document reported "no dedup signal" -- the same as a full retain -- even when nothing was re-extracted. Anything that meters by this field counted the whole document again on every re-retain. The streaming producer now counts what it actually sends to extraction (chunk text + context, as `_count_delta_content_tokens` does) and reports that count whenever it skipped at least one committed chunk. With nothing skipped it still returns None, so a first retain is unchanged. Tests: an unchanged oversized re-retain reports 0, and an oversized re-retain with one edited section plus an appended tail reports no less than what reached extraction and well under the submission. Both fail without the fix. Claude-Session: https://claude.ai/code/session_011Fo4NboRJPywxb1UfwpneJ
Code-review fixes on top of the fix: - `_streaming_retain_batch` reported the count only when at least one chunk was skipped. A sub-batch whose chunks are all new — the appended tail of a grown document — then returned None, and one None re-inflates the whole retain to the full submission via `merge_processed_content_tokens`. The condition is now `is_recovery`: once the producer has classified every chunk against the committed hashes, what it sent to extraction is a real measurement whether or not anything happened to match. Drops the `skipped_chunks` box. - The per-chunk token counting now runs only in recovery mode, so a first retain no longer tokenizes the whole body a second time for a number the return discards. - `_RetainResultCapture` was copy-pasted in three test modules (the PR added the third). Hoisted to `tests/retain_result_capture.py` and imported.
The agno v0.4.20 release added changelog/integrations/agno without the link its doc page is checked for, so check-integrations.mjs fails build-docs on every PR based on main.
nicoloboschi
force-pushed
the
fix/oversized-retain-processed-tokens
branch
from
October 7, 2026 08:02
58d7b98 to
b028093
Compare
This was referenced Oct 8, 2026
Merged
This was referenced Oct 16, 2026
Merged
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.
Problem
RetainResult.processed_content_tokenstells a post-retain hook how much content actually went through extraction after chunk-level dedup:None: no dedup signal; assume the full submission was processed.0: nothing changed.N: only N tokens were extracted.For re-retains of oversized documents it is wrong:
retain_batch_tokens(default 10,000) is split into sub-batches by the async retain path.attempts_delta_retain)._streaming_retain_batch. There they find their chunks already committed (the per-chunk recovery hashes) and skip them, but that function returnedprocessed_content_tokens=Noneunconditionally.Noneis contagious across sub-batches (merge_processed_content_tokens), so the whole retain reports "no dedup signal".The result is that a large document re-submitted with little or no change (for example, a growing transcript upserted under a stable
document_id) is reported exactly like a full first retain, even though almost nothing is re-extracted. Any extension that meters by this field counts the whole document again on every re-retain.Fix
_streaming_retain_batch's producer already decides, per chunk, whether to skip it (hash committed) or send it to extraction. In recovery mode — the document row'scontent_hashmatched and chunks are already committed, soexisting_chunk_hashesis non-empty — it now also counts the content + context tokens of the chunks it sends, the same quantity_count_delta_content_tokensreports on the delta path, and returns that instead ofNone.Outside recovery there is no per-chunk dedup at all, so the return stays
None— "bill the submitted payload" — which is also what every path that never reaches the producer keeps doing.The condition is recovery rather than "at least one chunk was skipped": a sub-batch whose chunks all differ from the committed ones (the appended tail of a grown document) did extract all of them, and saying so is accurate, where one
Nonewould re-inflate the whole retain back to the full submission. Counting is skipped entirely outside recovery, so a first retain does not tokenize the body a second time for a number that is thrown away.No change to what is extracted, chunked or stored; only the reported number changes.
One behavioural note: a retain resumed after a crash also skips the chunks its earlier attempt committed. It now reports only the remainder, consistent with what the delta path already reports for unchanged chunks.
Tests
Two tests added to
tests/test_delta_oversized_replacement.py:0. Before this change it reportedNonewith zero LLM input tokens.Both fail without the fix and pass with it.
The
_RetainResultCapturevalidator these need was already copy-pasted intest_delta_retain.pyandtest_document_transfer.py; it now lives once intests/retain_result_capture.pyand all three import it.https://claude.ai/code/session_011Fo4NboRJPywxb1UfwpneJ