fix: flatten sync_turn content to enable turn-aware chunking for hindsight plugin - #51270
fix: flatten sync_turn content to enable turn-aware chunking for hindsight plugin#51270qxxaa wants to merge 3 commits into
Conversation
|
Tested on my environment with a live Hindsight deployment. After applying this fix, conversation chunks now consistently start with Before the fix, the nested There's a complementary server-side fix I've submitted separately to Hindsight (vectorize-io/hindsight#2412) to handle the |
|
Hi, the complementary server-side fix for this (vectorize-io/hindsight#2412) merged back in June. Without this PR, the client is still sending nested arrays that bypass Hindsight's turn-aware chunking, causing it to fall back to blind text splitting and misattribute speakers. The data integrity issue is on the sending side. Would appreciate this getting another look |
|
Hi @alt-glitch - would you consider bumping this to P2? I don't think P3/cosmetic is the right fit here. This is a data integrity bug, not a cosmetic issue. Without this fix, every Hindsight retain cycle sends nested arrays ( The server-side companion fix (vectorize-io/hindsight#2412) merged back in June. That handles the It's a one-line change with three updated tests and zero regression risk. Happy to address any feedback if needed. |
|
Thanks for the focused fix. The premise is verified on current main: Problems
Suggested changes
Automated hermes-sweeper review. |
4c3bbfe to
7d5594f
Compare
|
Added the flat-dict contract assertion to Full test suite passes (117/117). Note: the unrelated |
7d5594f to
26a56a1
Compare
|
Rebased onto current main to absorb the |
2e60a16 to
f8c91b9
Compare
9e50c9a to
0bf0346
Compare
sync_turn serialises each turn pair as a JSON array and stores it in
_session_turns. At retain time, joining these produces a nested array:
[[{user},{asst}],[{user},{asst}],...]. Hindsight's chunk_text() detects
conversation arrays via:
all(isinstance(turn, dict) for turn in parsed)
The nested structure fails this check (outer elements are lists, not
dicts) and falls through to RecursiveCharacterTextSplitter which splits
on sentence/paragraph boundaries with no turn awareness.
This causes chunks to begin mid-message without speaker attribution.
The extraction LLM then misattributes the orphaned text — e.g.
assigning an assistant's first-person statement to the user.
Fix: store each turn as comma-joined JSON objects rather than a JSON
array. The existing join sites ('[' + ','.join(...) + ']') now produce
a flat array of message dicts that passes the isinstance check and
routes to _chunk_conversation(), which packs complete turns and never
splits mid-message.
Verified: chunks produced after this fix all begin with complete
{\"role\": ..., \"content\": ..., \"timestamp\": ...} objects with full
speaker prefixes intact."
Add flat-dict contract assertion to session-switch flush test as suggested by sweeper review.
0bf0346 to
6140dc9
Compare
Problem
sync_turn()produces a nested JSON array when building the retain payload:Hindsight's server-side
chunk_text()(inengine/retain/fact_extraction.py) detects conversation format via:This detection has no fallback handling for nested arrays. It silently fails the
isinstance(turn, dict)check (outer elements are lists, not dicts) and drops through toRecursiveCharacterTextSplitter. The text splitter has no awareness of conversation turn boundaries and can split mid-message, producing chunks that begin without speaker attribution.When an extraction LLM receives a chunk with no role prefix, it has no basis for determining who said what. Any first-person statement in such a fragment will be attributed to whichever speaker the model guesses, and that guess propagates through consolidation as a persistent fact.
Fix
One-line change in
sync_turn(). Store each turn as comma-joined JSON objects instead of a JSON array:The two join sites (
sync_turnline 1571,on_session_switchline 1759) already wrap with"[" + ",".join(...) + "]"and now produce a flat array:[{"role":"user","content":"Q: ...","timestamp":"..."},{"role":"assistant","content":"JARVIS: ...","timestamp":"..."},...]This passes the
all(isinstance(turn, dict))check and routes to_chunk_conversation(), which packs complete turns without splitting mid-message.Affected code paths
sync_turn()The join sites at lines 1571 (
sync_turnretain) and 1759 (on_session_switchflush) are unchanged. They produce correct output once the stored elements are flat.Regression risk
None. The change only affects the internal serialisation format of
_session_turnsentries. The finalcontentstring sent toaretain_batchis still a valid JSON array of message dicts. The only difference is nesting depth. Hindsight's extraction pipeline processes the same message objects regardless of how they arrived.Test changes
test_sync_turn_retains_metadata_rich_turn- assertions now reference flat indices (content[0]notcontent[0][0]).test_sync_turn_every_n_turns-len(content) == 6(3 turns x 2 messages) instead oflen(content) == 3(3 pairs).test_sync_turn_produces_flat_conversation_array- explicitly verifies the flat-dict contract that enables Hindsight's turn-aware chunking path.