fix(retain): merge JSON arrays in append mode to preserve conversation-aware chunking - #2412
Merged
Conversation
…n-aware chunking When update_mode=append prepends existing document text as a second content item, combined_content is built with "\n".join(...). For conversation-format content (flat JSON arrays of message dicts), this produces "[...]\n[...]" which is not valid JSON. On subsequent append cycles, chunk_text() fails to parse the corrupted original_text. _chunk_jsonl() also rejects it (lines are arrays, not dicts). The text falls through to RecursiveCharacterTextSplitter, which splits on sentence boundaries with no awareness of conversation turn structure. This produces chunks that begin mid-sentence without speaker attribution, causing the extraction LLM to misattribute statements. Fix: after the append-mode block assembles contents_dicts with the existing and new content items, detect when all items are JSON arrays of dicts and merge them into a single flat array. Non-conversation content (plain text, JSONL) is unaffected. close vectorize-io#2409
Add tests for chunking newline-joined and merged JSON arrays.
This test ensures that appending conversation arrays maintains the original_text as a valid flat JSON array after multiple append cycles, preventing degradation of the data structure.
nicoloboschi
approved these changes
Jun 26, 2026
nicoloboschi
left a comment
Collaborator
There was a problem hiding this comment.
Confirmed the bug and traced it through the code. Forward fix is correct: merging the JSON arrays keeps original_text valid so subsequent appends route back through _chunk_conversation. Tests pass locally. Healing of pre-existing corrupted docs is intentionally out of scope.
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
When
update_mode=appendis used with conversation-format content (flat JSON arrays of{role, content, timestamp}dicts),original_textbecomes invalid JSON after the second retain cycle.In
_streaming_retain_batch,combined_contentis built with:For conversation content, this produces
[...]\n[...]- two JSON arrays joined by a newline. This is neither valid JSON nor valid JSONL (lines are arrays, not dicts).On subsequent append cycles,
chunk_text()fails to parse the corruptedoriginal_text._chunk_jsonl()also rejects it. The text falls through toRecursiveCharacterTextSplitter, which splits on sentence boundaries (.) inside message content with no awareness of conversation turn structure. Chunks begin mid-sentence without speaker attribution, and the extraction LLM misattributes statements.Fix
In
orchestrator.py, in_streaming_retain_batch, after the append-mode block assemblescontents_dicts = [existing_content, *contents_dicts]: detect when all content items are JSON arrays of dicts, parse them, merge into a single flat array, and re-serialize.Non-conversation content (plain text, JSONL, single JSON objects) is unaffected - the
isinstancecheck fails and the block is skipped.Tests
test_chunking.pyandtest_retain_append_mode.pyverify both the corrupted path (regression guard) and the merged path (correct routing through_chunk_conversation).test_newline_joined_json_arrays_bypass_conversation_chunkingis intentionally a regression document (proves the broken path exists), not a behavioural assertion. The second test (test_merged_json_array_routes_to_conversation_chunking) validates the correct routing.max_chars=120in the routing test is budget-sensitive but intentionally tight to force a multi-chunk split with short test messages.Closes #2409