Skip to content

fix(chat): reload fidelity for streamed replies — model/sources, turn order, backfill (#13292, #13293, #13295, #13296) - #13303

Merged
mrveiss merged 8 commits into
Dev_new_guifrom
issue-13292
Aug 2, 2026
Merged

mrveiss merged 8 commits into
Dev_new_guifrom
issue-13292

Conversation

@mrveiss

@mrveiss mrveiss commented Aug 2, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

#13214 made the completed streamed reply reach chat:session:* at all. What it wrote was bare-bones: metadata that existed only on the discarded streaming chunks never made it into the persisted entry. This PR fixes that, and — after review — deliberately does not attempt two of the four issues it started with.

#13295 and #13293 are NOT closed here — review round 2

#13295 (reload order) was originally "fixed" by inserting the reply at the front of the batch. Review showed the premise is false for the common case. The reply is chronologically first only on a single-iteration turn. A tool-using turn normally runs two iterations (manager.py:2841; _check_continuation_decision at :2515-2555 returns True after a normal tool execution), each appending its own prose (:2864), and every segment is collapsed into one string by "\n\n".join(all_llm_responses) (:3338, graph.py:1315). Live order is prose1 -> tool output -> prose2(the answer). Front-insertion therefore put the final answer before the tool output it was derived from, and left the session ending on a sender="system" terminal dump — a user-visible regression on the most common tool path.

Reverted to batch.append(final_entry). test_reply_appended_after_tool_output keeps its original assertion, with a docstring recording why front-insertion is wrong. Added test_two_iteration_turn_reload_order_matches_live_order as a documented baseline for the real fix, which needs per-iteration data (all_llm_responses: list[str] plus an iteration marker on workflow_messages) and is its own change.

#13293 (backfill) had two defects, both found by the reviewer running the module:

  1. It double-posted into healthy sessions. Damage was judged by checking only i+1, but pre-bug(chat): streamed AutoBot replies are never persisted — session history stores user turns only, so every reply vanishes on reload #13214 tool turns persist terminal_output as sender="system" (manager.py:2967-2978), so a turn that did save its reply reads back as [user, system, assistant] and looked damaged — inserting a duplicate reply into a session needing no repair.
  2. It could not repair the sessions it was built for. It read only chat:conversation:{id}, which has a 24h TTL, while every session damaged by bug(chat): streamed AutoBot replies are never persisted — session history stores user turns only, so every reply vanishes on reload #13214 is by definition in the past. It would have reported 0 and exited cleanly.

Both fixed below, but the module has still never run against real data, so it does not clear closure Gate 3. #13293 stays open with a task to run it against real damaged sessions and attach before/after evidence.

What Changed

autobot-backend/chat_workflow/manager.py

autobot-backend/chat_workflow/graph.py — persist_conversation threads the same three values from state; decomposed into _emit_before_response_send_safe, _emit_after_response_send_safe, _build_workflow_messages_from_state.

autobot-backend/chat_history/session_reply_backfill.py (new, unwired)

  • _find_turn_end() + turn-window scan: a turn is damaged only when no assistant entry exists before the next user message. Insertion at turn_end, after tool/system entries.
  • _load_conversation_pairs(): Redis chat:conversation:* first, falling back to _load_transcript_pairs() reading the full data/conversation_transcripts/{id}.json (durable, no TTL — unlike production's last-10-truncated loader).
  • Documented as an unlocked read-modify-write: run only against an idle session.

autobot_shared/ssot_constants.py — added REDIS_KEY.CHAT_CONVERSATION_PREFIX; chat_workflow/conversation.py and tasks/chat_retention.py now import it, removing a triplicated literal.

Verification

After: chat_workflow/ chat_history/ api/chat_phase2_test.py api/chat_phase4_test.py -> 460 passed.

Before (red state): production code reverted via git checkout with test files kept at their fixed versions -> 10 failures, including the double-post reproduction (assert 1 == 0) and TypeError: unexpected keyword argument 'transcript_dir' on every transcript-fallback test. All 10 pass after restoring the fix.

The backfill fake store subclasses the real MessagesMixin, so _build_message_dict/add_messages_batch are production code and only the load/save seam is in-memory. Idempotency is covered both as pure logic and end-to-end.

black, isort, flake8 clean on every touched file.

Not verified: the repair module against a real Redis instance or real filesystem — only tmp_path and fake-Redis/fake-store paths were exercised, per the constraint never to run it against a live system. That is precisely why #13293 remains open.

Model Used

Sonnet 5

Closes #13292, #13296

mrveiss added 3 commits August 2, 2026 16:51
…r streamed replies (#13292, #13295, #13296)

- #13292: the completed streamed reply persisted with sources: [] and no
  model in metadata because that data lived only on the discarded streaming
  chunks. _build_final_response_entry now threads selected_model/
  rag_citations/used_knowledge from the same LLMIterationContext/graph state
  the caller already has, so the persisted turn carries the model badge and
  KB citations the user actually saw.
- #13295: on tool-using turns the completed reply is chronologically the
  FIRST thing the user watched stream (prose, then any tool call/output) but
  was appended LAST in the persisted batch, inverting the reload order.
  _persist_workflow_messages now inserts it at the front of the batch.
  Updated the enshrining test (test_reply_appended_after_tool_output ->
  test_reply_precedes_tool_output_on_reload) which locked in the inversion.
- #13296: extracted the duplicated citation-mapping block into a shared
  _kb_sources_from_citations() helper, reused by both the per-message
  citation list and the new final-entry sources.
- Updated the _FakeManager test double in internal_prompt_filter_test.py
  (positional/no-arg stubs) to match the new call signature.
…13214 (#13293)

Sessions written while the #13214 bug was live have chat:session:* holding
user turns with no assistant reply — the completed streamed reply was never
persisted. chat:conversation:* (the short-lived {user, assistant} pair cache
used for LLM context) was unaffected and still holds the missing text while
its TTL has not expired, so it is the backfill source.

session_reply_backfill.py is a pure repair path: compute_backfilled_messages
is I/O-free and independently tested; repair_session owns the Redis
read + chat_mgr load/save seam and is injectable for tests. The CLI entry
point requires explicit --session-id arguments (no bulk auto-scan) and is
not invoked anywhere in this change — this PR adds the repair logic and its
tests only, per #13293, and does not run it against any system.

Also extracted _build_workflow_message_batch out of _persist_workflow_messages
(Extract Method, continuing #13296) now that the latter has three more
keyword arguments to thread through.
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

mrveiss and others added 5 commits August 2, 2026 17:44
…x, fix backfill double-post, add transcript fallback

BLOCKER 1 (#13295): the front-insertion fix assumed the reply is always
chronologically first, which only holds for a single-iteration tool turn.
The common case is 2+ iterations (prose1, tool_output, prose2-the-answer,
collapsed into one string by "\n\n".join(all_llm_responses)), where the
combined prose legitimately belongs AFTER the tool output. Reverted to
append (matches pre-PR); rewrote _persist_workflow_messages/
_build_persist_batch docstrings to record the finding; restored
test_reply_appended_after_tool_output's original assertion with a docstring
explaining both cases, and added test_two_iteration_turn_reload_order_matches_live_order
as a baseline for the real interleaving fix (needs per-iteration data on a
future signature - tracked on #13295, not fixed here).

BLOCKER 2: compute_backfilled_messages checked only the immediately-following
message slot for an existing reply, so a healthy tool turn persisted as
[user, system(terminal_output), assistant] was misjudged damaged and got a
duplicate assistant message inserted. Now scans the full turn window (up to
the next user message, or end of session) for ANY assistant entry, and
inserts a genuinely-missing reply at the end of that window - after tool/
system entries, not immediately after the user message.

BLOCKER 3: chat:conversation:* (Redis, 24h TTL) is not "the only remaining
source" - every session damaged by #13214 is in the past, so this key has
almost certainly expired by the time anyone runs this repair. Added
_load_transcript_pairs, a fallback to the durable
data/conversation_transcripts/{id}.json file (no TTL, full history - unlike
the production loader's last-10 truncation, which exists only to bound LLM
context). repair_session now tries Redis first, then the transcript file.

Also (non-blocking, requested):
- Extracted REDIS_KEY.CHAT_CONVERSATION_PREFIX in autobot_shared/ssot_constants.py
  as the single source for the chat:conversation: prefix, previously
  duplicated in chat_workflow/conversation.py, tasks/chat_retention.py, and
  this module.
- session_reply_backfill_test.py's fake chat-history store now subclasses
  the real MessagesMixin (matching streamed_reply_persistence_test.py)
  instead of a hand-rolled duck-typed _build_message_dict that silently
  omitted author_id.
- Added an idempotency test (repair_session called twice backfills 0 the
  second time) at both the pure-logic and end-to-end level.
- Documented repair_session as an unlocked read-modify-write - run only
  against a session with no concurrent writer.
- Decomposed graph.py's persist_conversation (extracted
  _emit_before_response_send_safe/_emit_after_response_send_safe/
  _build_workflow_messages_from_state) and manager.py's
  _persist_workflow_messages (extracted _build_persist_batch) to shrink
  both toward the project's function-length convention.

Revised scope: only #13292 and #13296 are fully delivered by this PR.
#13295 (ordering) and #13293 (backfill) remain open per review.
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.

1 participant