Skip to content

fix(cache): don't promote truncated LLM chunks to the semantic cache as complete#1950

Closed
TPAteeq wants to merge 1 commit into
Graphify-Labs:v8from
TPAteeq:fix/partial-extraction-cache-promotion
Closed

fix(cache): don't promote truncated LLM chunks to the semantic cache as complete#1950
TPAteeq wants to merge 1 commit into
Graphify-Labs:v8from
TPAteeq:fix/partial-extraction-cache-promotion

Conversation

@TPAteeq

@TPAteeq TPAteeq commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Problem

When an LLM chunk is truncated (finish_reason="length") and can't be recovered by splitting — or the adaptive-retry recursion hits its depth cap — the extractor keeps the partial node set it managed to parse. That partial result was then checkpointed and written to the content-hash semantic cache and manifest-stamped as a complete extraction. Because the cache is keyed by file content, the incomplete node set is served on every subsequent run forever — until the file's content changes or the user passes --force. Half a document's symbols silently masquerade as the whole, with no persisted signal that anything was missing.

Fix

A truncated give-up result is tagged with an internal _partial marker on each of its items. save_semantic_cache stamps the affected file's cache entry partial: True (detected either from that marker or from an explicit partial_source_files argument), and load_cached treats a partial entry as a cache miss, so the file is re-dispatched and retried instead of replayed. To make that re-dispatch actually happen on the common path, the partial file is also left unstamped in the manifest — mirroring the existing failed-chunk handling (#933) — so detect_incremental re-queues it on the next incremental update/extract, not only on a full/--force/content-change run.

A file split into slices across chunks is handled correctly: the merge_existing checkpoint peeks at a partial previous entry (load_cached(allow_partial=True)) so it accumulates all slices and keeps the entry partial, rather than a later clean slice silently dropping a truncated one and promoting the half-file to complete. The whole thing self-heals — a later complete extraction overwrites the same content-hash key with a non-partial entry — and the internal marker is stripped after the final save so it never leaks into graph.json.

Tests

tests/test_partial_cache.py and additions to tests/test_chunking.py: partial entry is a load miss; explicit partial_source_files; self-heal on complete re-extraction; sliced-file accumulation stays partial without losing a slice; manifest excludes partial files; the marker helpers; and an end-to-end truncated extract_corpus_parallel checkpoint that reloads as a miss. Full test suite green (3297 passed).

…as complete

A chunk whose LLM response is truncated (`finish_reason="length"`) and can't be
recovered by splitting, or that hits the adaptive-retry depth cap, returns a
partial node set. Today that set is checkpointed and written to the content-hash
semantic cache + manifest-stamped as complete, so the incomplete nodes are
served forever until the file content changes or `--force`.

Truncated give-up results are now tagged with an internal `_partial` marker.
`save_semantic_cache` stamps the affected file's entry `partial: True` (detected
from the marker or an explicit `partial_source_files` arg), and `load_cached`
treats a partial entry as a cache MISS, so the file re-dispatches and retries.
The file is also left unstamped in the manifest (like a failed chunk, Graphify-Labs#933) so
detect_incremental re-queues it on the next incremental run — not only on a full
/ `--force` / content-change run. A file sliced across chunks accumulates via a
partial-aware `merge_existing` peek (`load_cached(allow_partial=True)`) so a
truncated slice is never dropped or silently promoted to complete. Self-heals: a
later complete extraction overwrites the same key with a non-partial entry. The
marker is stripped after the final save so it never leaks into graph.json.
safishamsi added a commit that referenced this pull request Jul 16, 2026
…#1950)

The _partial item-marker approach couldn't fire on the most common truncation
shape: a mid-JSON cut parses to zero items, so a sliced document whose second
slice truncated empty was still stamped complete (the PR's own headline case).

- The adaptive-retry give-up sites now record the chunk's own source files in a
  result-level _partial_files list, independent of parsed items; it propagates
  through _merge_two / the recursion merges / _merge_into so it reaches both the
  per-chunk checkpoint and the run-level manifest stamp.
- _partial_source_files unions _partial_files with the item markers.
- save_semantic_cache seeds an empty group for a named partial file with no
  items so its entry is stamped partial, and carries a partial prev entry's flag
  forward so a later clean slice merging over it can't re-promote it to complete.
- the CLI final save now passes partial_source_files (computed before the save)
  so an empty-parse file isn't written back as a complete entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Collaborator

Landed on v8 (ships in 0.9.18), cherry-picked to preserve your authorship. Thanks @TPAteeq — truncated chunks are no longer cached/stamped as complete. Follow-up added for the empty-parse case: a mid-JSON cut parses to zero items, so the give-up sites now record the chunk own files independently of parsed items (propagated through the merges), the cache stamps a partial file even with no items, and a later clean slice cannot re-promote a partial entry. That closes the sliced-document scenario end to end.

@safishamsi safishamsi closed this Jul 16, 2026
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.

2 participants