* fix(transparent-stream): drop stale final-answer prefix row in multi-segment settle (#5749 follow-up) A turn with interim assistant messages (prose interleaved with tool calls) could show the beginning of the final answer TWICE after watching it stream: once as a settled anchor-scene prose row (the live-token accumulator's last throttled snapshot) and once as the real assistant segment. The duplicate persisted until reload. Root cause: #5758 suppresses the accumulator row only when it sits "after the last tool row", but _completeSettledAnchorSceneForTurn appends the settled per-message tool rows AFTER the projected live rows — those re-list tools that ran EARLIER in the turn, pushing the boundary past the final segment's accumulator so the guard never fired. The stale prefix snapshot then survived into the persisted scene and rendered above the settled answer. Fix: judge final-segment eligibility against the LIVE projection's own chronology — a live-prose row belongs to the final segment iff no PROJECTED tool row follows it. Pre-tool narration that happens to prefix the final answer stays protected (existing #5758 regression tests still pass), and a new regression test pins the multi-segment shape. Verified end-to-end by replaying the captured run journal of an affected session through the real SSE live handlers in headless Chromium: duplicate before (answer prefix visible twice after settle), gone after; fresh-reload rendering unchanged. Rollback: revert this commit; behavior returns to pre-fix (duplicate prefix row after live-settle of multi-segment turns). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * CHANGELOG: transparent-stream multi-segment prefix dedupe (#6189, @ai-ag2026) --------- Co-authored-by: ai-ag2026 <m.fuechtenkoetter@posteo.de> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
48 lines
2.2 KiB
Python
48 lines
2.2 KiB
Python
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _function_body(src: str, name: str) -> str:
|
|
marker = f"function {name}"
|
|
start = src.index(marker)
|
|
brace = src.index("{", start)
|
|
depth = 0
|
|
for idx in range(brace, len(src)):
|
|
if src[idx] == "{":
|
|
depth += 1
|
|
elif src[idx] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return src[brace + 1 : idx]
|
|
raise AssertionError(f"{name} body not found")
|
|
|
|
|
|
def test_settled_scene_keys_live_token_prefix_dedupe_to_final_answer_identity():
|
|
settle_body = _function_body(MESSAGES_JS, "_completeSettledAnchorSceneForTurn")
|
|
final_overlap_body = _function_body(MESSAGES_JS, "_anchorSceneRowLooksLikeFinalAnswer")
|
|
|
|
# The final-segment boundary must come from the LIVE projection's own
|
|
# chronology (projectedRows), not the combined orderedRows: the settled
|
|
# per-message tool rows appended there re-list tools that ran earlier in
|
|
# the turn and would push the boundary past the final segment's live-prose
|
|
# accumulator (the #5758 multi-segment gap).
|
|
assert "lastProjectedToolIndex=projectedRows.reduce" in settle_body
|
|
assert "finalSegmentLiveProseRows" in settle_body
|
|
assert "rowIsLiveTokenFinalPrefix(row,textKey,finalSegmentEligible)" in settle_body
|
|
assert "rowHasNonLiveDuplicate" not in settle_body
|
|
assert "_anchorSceneRowLooksLikeFinalAnswer(textKey,finalKey)" in settle_body
|
|
assert "(shorter/longer)>=0.9" in final_overlap_body
|
|
|
|
|
|
def test_render_scene_passes_final_segment_eligibility_to_live_prefix_guard():
|
|
ui_js = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
render_body = _function_body(ui_js, "_renderSettledAnchorSceneTransparentForMessage")
|
|
row_body = _function_body(ui_js, "_anchorSceneTransparentNodeForRow")
|
|
|
|
assert "const lastNonTerminalWorkRowIndex=_anchorSceneLastNonTerminalWorkRowIndex(rows);" in render_body
|
|
assert "liveTokenFinalPrefixEligible:idx>lastNonTerminalWorkRowIndex" in render_body
|
|
assert "opts&&opts.liveTokenFinalPrefixEligible&&_anchorSceneLiveTokenFinalPrefix" in row_body
|