Files
hermes-webui/tests/test_issue5749_transparent_stream_prefix_dedupe.py
T
52c962cf7d Release: Transparent Stream multi-segment prefix dedupe (#6189, @ai-ag2026) (#6217)
* 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>
2026-07-17 21:51:22 -07:00

812 lines
30 KiB
Python

"""Regression tests for issue #5749 Transparent Stream prefix dedupe."""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
NODE = shutil.which("node")
ISSUE5749_CAPTURED_SESSION = json.loads(
(ROOT / "tests" / "fixtures" / "issue5749_captured_session_prefix.json").read_text(encoding="utf-8")
)
def _run_node(src, script, tmp_path):
assert NODE, "node is required for issue #5749 regression tests"
script_path = tmp_path / "issue5749_node_script.js"
script_path.write_text(script, encoding="utf-8")
result = subprocess.run([NODE, str(script_path)], text=True, capture_output=True, check=False)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def _normalized_text(text):
return " ".join(str(text or "").split()).lower()
def _issue5749_captured_scene():
scenes = ISSUE5749_CAPTURED_SESSION["anchor_activity_scenes"]
scene = next(iter(scenes.values()))["scene"]
rows = scene["activity_rows"]
assert len(rows) == 1
row = rows[0]
assert row["role"] == "prose"
assert row["kind"] == "process_prose"
assert row["source_event_type"] == "token"
assert row["local_id"].startswith("live-prose:")
assert not any(
other is not row and other.get("text") == row["text"] and not str(other.get("local_id", "")).startswith("live-prose:")
for other in rows
)
row_key = _normalized_text(row["text"])
final_key = _normalized_text(scene["final_answer"])
assert row_key and final_key.startswith(row_key)
assert 0.40 <= len(row_key) / len(final_key) <= 0.45
return scene, row
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_reproduction_fixture_matches_lone_live_prefix_shape():
scene, row = _issue5749_captured_scene()
final_key = _normalized_text(scene["final_answer"])
row_key = _normalized_text(row["text"])
assert row_key != final_key
assert final_key.startswith(row_key)
assert scene["final_answer"] == ISSUE5749_CAPTURED_SESSION["messages"][0]["content"]
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_settlement_preserves_pre_tool_live_token_prefix_rows(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(MESSAGES_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
global.window = {{
chatActivityMode() {{ return 'transparent_stream'; }},
_chatActivityDisplayMode: 'transparent_stream',
_transparentStream: true,
}};
global.S = {{ session: {{}} }};
eval(extractFunc('_anchorSceneCleanText'));
eval(extractFunc('_anchorSceneTextKey'));
eval(extractFunc('_anchorSceneExistingRowKey'));
eval(extractFunc('_anchorSceneRowHasLiveIdentity'));
eval(extractFunc('_anchorSceneSettleLiveRunningRow'));
eval(extractFunc('_anchorSceneRowLooksLikeFinalAnswer'));
eval(extractFunc('_anchorSceneRowTextOverlapsExisting'));
eval(extractFunc('_anchorSceneMessageRowsHaveThinking'));
eval(extractFunc('_completeSettledAnchorSceneForTurn'));
function _anchorSceneActiveMode() {{ return 'transparent_stream'; }}
function _anchorSceneFinalAnswerText(message) {{ return message && (message.final_answer || message.content || ''); }}
function _anchorSceneRowsByMessageIndex() {{ return new Map(); }}
function _anchorSceneMessageRef(message) {{ return String(message && message.id || ''); }}
function _anchorSceneTurnDurationForSettlement() {{ return 0; }}
function _anchorSceneRowDisplayHintForMode(row, sceneMode) {{
const hints = row && typeof row === 'object' && row.display_hints && typeof row.display_hints === 'object' ? row.display_hints : null;
if (sceneMode === 'transparent_stream') return (hints && hints.transparent_stream) || 'chronological_activity';
if (sceneMode === 'compact_worklog') return (hints && hints.compact_worklog) || row && row.display_hint || 'activity_row';
return row && row.display_hint || 'activity_row';
}}
const messages = [
{{ role: 'user', content: 'Prompt', id: 'user-1' }},
{{ role: 'assistant', content: {json.dumps(final_answer)}, id: 'assistant-1' }},
];
const scene = _completeSettledAnchorSceneForTurn(messages, 1, {{
mode: 'transparent_stream',
final_answer: {json.dumps(final_answer)},
lifecycle: {{ terminal_state: 'done' }},
identity: {{ source_message_refs: ['legacy'] }},
activity_rows: [
{{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: {json.dumps(fixture_row["local_id"])},
text: {json.dumps(prefix_text)},
status: 'running',
attachments: [{{ id: 'attachment-1' }}],
}},
{{
role: 'tool',
kind: 'tool_result',
source_event_type: 'tool',
local_id: 'tool-row-1',
text: 'Fetched docs',
status: 'running',
}},
],
}});
process.stdout.write(JSON.stringify({{
final_answer: scene.final_answer,
rows: scene.activity_rows.map(row => ({{
role: row.role,
kind: row.kind,
source_event_type: row.source_event_type,
local_id: row.local_id,
text: row.text,
status: row.status,
attachments: row.attachments || null,
}})),
}}));
"""
data = _run_node(MESSAGES_JS, script, tmp_path)
assert data["final_answer"] == final_answer
assert [row["local_id"] for row in data["rows"]] == [fixture_row["local_id"], "tool-row-1"]
assert data["rows"][0]["text"] == prefix_text
assert data["rows"][0]["role"] == "prose"
assert data["rows"][1]["role"] == "tool"
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_distinct_live_token_prefix_rows_survive_settlement(tmp_path):
final_answer = "The solution is to preserve short legitimate live-token prefixes while still suppressing longer duplicated final-answer spans."
prefix_text = "Checking context"
script = f"""
const src = {json.dumps(MESSAGES_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
global.window = {{
chatActivityMode() {{ return 'transparent_stream'; }},
_chatActivityDisplayMode: 'transparent_stream',
_transparentStream: true,
}};
global.S = {{ session: {{}} }};
eval(extractFunc('_anchorSceneCleanText'));
eval(extractFunc('_anchorSceneTextKey'));
eval(extractFunc('_anchorSceneExistingRowKey'));
eval(extractFunc('_anchorSceneRowHasLiveIdentity'));
eval(extractFunc('_anchorSceneSettleLiveRunningRow'));
eval(extractFunc('_anchorSceneRowLooksLikeFinalAnswer'));
eval(extractFunc('_anchorSceneRowTextOverlapsExisting'));
eval(extractFunc('_anchorSceneMessageRowsHaveThinking'));
eval(extractFunc('_completeSettledAnchorSceneForTurn'));
function _anchorSceneActiveMode() {{ return 'transparent_stream'; }}
function _anchorSceneFinalAnswerText(message) {{ return message && (message.final_answer || message.content || ''); }}
function _anchorSceneRowsByMessageIndex() {{ return new Map(); }}
function _anchorSceneMessageRef(message) {{ return String(message && message.id || ''); }}
function _anchorSceneTurnDurationForSettlement() {{ return 0; }}
function _anchorSceneRowDisplayHintForMode(row, sceneMode) {{
const hints = row && typeof row === 'object' && row.display_hints && typeof row.display_hints === 'object' ? row.display_hints : null;
if (sceneMode === 'transparent_stream') return (hints && hints.transparent_stream) || 'chronological_activity';
if (sceneMode === 'compact_worklog') return (hints && hints.compact_worklog) || row && row.display_hint || 'activity_row';
return row && row.display_hint || 'activity_row';
}}
const messages = [
{{ role: 'user', content: 'Prompt', id: 'user-1' }},
{{ role: 'assistant', content: {json.dumps(final_answer)}, id: 'assistant-1' }},
];
const scene = _completeSettledAnchorSceneForTurn(messages, 1, {{
mode: 'transparent_stream',
final_answer: {json.dumps(final_answer)},
lifecycle: {{ terminal_state: 'done' }},
identity: {{ source_message_refs: ['legacy'] }},
activity_rows: [
{{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: 'live-prose:stream-short:1',
text: {json.dumps(prefix_text)},
status: 'running',
}},
],
}});
process.stdout.write(JSON.stringify(scene.activity_rows.map(row => row.text)));
"""
data = _run_node(MESSAGES_JS, script, tmp_path)
assert data == [prefix_text]
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_long_live_progress_prefix_is_suppressed_without_settled_duplicate(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(MESSAGES_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
global.window = {{
chatActivityMode() {{ return 'transparent_stream'; }},
_chatActivityDisplayMode: 'transparent_stream',
_transparentStream: true,
}};
global.S = {{ session: {{}} }};
eval(extractFunc('_anchorSceneCleanText'));
eval(extractFunc('_anchorSceneTextKey'));
eval(extractFunc('_anchorSceneExistingRowKey'));
eval(extractFunc('_anchorSceneRowHasLiveIdentity'));
eval(extractFunc('_anchorSceneSettleLiveRunningRow'));
eval(extractFunc('_anchorSceneRowLooksLikeFinalAnswer'));
eval(extractFunc('_anchorSceneRowTextOverlapsExisting'));
eval(extractFunc('_anchorSceneMessageRowsHaveThinking'));
eval(extractFunc('_completeSettledAnchorSceneForTurn'));
function _anchorSceneActiveMode() {{ return 'transparent_stream'; }}
function _anchorSceneFinalAnswerText(message) {{ return message && (message.final_answer || message.content || ''); }}
function _anchorSceneRowsByMessageIndex() {{ return new Map(); }}
function _anchorSceneMessageRef(message) {{ return String(message && message.id || ''); }}
function _anchorSceneTurnDurationForSettlement() {{ return 0; }}
function _anchorSceneRowDisplayHintForMode(row, sceneMode) {{
const hints = row && typeof row === 'object' && row.display_hints && typeof row.display_hints === 'object' ? row.display_hints : null;
if (sceneMode === 'transparent_stream') return (hints && hints.transparent_stream) || 'chronological_activity';
if (sceneMode === 'compact_worklog') return (hints && hints.compact_worklog) || row && row.display_hint || 'activity_row';
return row && row.display_hint || 'activity_row';
}}
const messages = [
{{ role: 'user', content: 'Prompt', id: 'user-1' }},
{{ role: 'assistant', content: {json.dumps(final_answer)}, id: 'assistant-1' }},
];
const scene = _completeSettledAnchorSceneForTurn(messages, 1, {{
mode: 'transparent_stream',
final_answer: {json.dumps(final_answer)},
lifecycle: {{ terminal_state: 'done' }},
identity: {{ source_message_refs: ['legacy'] }},
activity_rows: [
{{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: {json.dumps(fixture_row["local_id"])},
text: {json.dumps(prefix_text)},
status: 'running',
}},
],
}});
process.stdout.write(JSON.stringify(scene.activity_rows.map(row => ({{
local_id: row.local_id,
text: row.text,
status: row.status,
}}))));
"""
data = _run_node(MESSAGES_JS, script, tmp_path)
assert data == []
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_render_fallback_suppresses_persisted_live_progress_prefix_rows(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(UI_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
class FakeElement {{
constructor(tag) {{
this.tagName = String(tag || 'div').toUpperCase();
this.attributes = Object.create(null);
this.dataset = Object.create(null);
}}
setAttribute(name, value) {{
this.attributes[name] = String(value);
if (name.startsWith('data-')) {{
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
this.dataset[key] = String(value);
}}
}}
}}
global.window = {{}};
global.document = {{ createElement(tag) {{ return new FakeElement(tag); }} }};
global._anchorSceneNodeForRow = () => new FakeElement('div');
global._decorateTransparentEventRow = node => node;
global._anchorSceneToolCallFromRow = () => ({{}});
global.buildToolCard = () => new FakeElement('div');
global._thinkingActivityNode = () => new FakeElement('div');
eval(extractFunc('_anchorSceneProseMatchesFinalAnswer'));
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
const liveRow = {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: {json.dumps(fixture_row["local_id"])},
text: {json.dumps(prefix_text)},
}};
const liveResult = _anchorSceneTransparentNodeForRow(liveRow, {{
settled: true,
finalAnswer: {json.dumps(final_answer)},
liveTokenFinalPrefixEligible: true,
}});
process.stdout.write(JSON.stringify({{
liveResult: liveResult === null,
}}));
"""
data = _run_node(UI_JS, script, tmp_path)
assert data["liveResult"] is True
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_render_fallback_suppresses_near_complete_live_prefix_rows(tmp_path):
final_answer = "I found the issue and I am fixing it by deduping live-token prefixes during settlement and render fallback now."
prefix_text = "I found the issue and I am fixing it by deduping live-token prefixes during settlement and render fallback"
script = f"""
const src = {json.dumps(UI_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
class FakeElement {{
constructor(tag) {{
this.tagName = String(tag || 'div').toUpperCase();
this.attributes = Object.create(null);
this.dataset = Object.create(null);
}}
setAttribute(name, value) {{
this.attributes[name] = String(value);
if (name.startsWith('data-')) {{
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
this.dataset[key] = String(value);
}}
}}
}}
global.window = {{}};
global.document = {{ createElement(tag) {{ return new FakeElement(tag); }} }};
global._anchorSceneNodeForRow = () => new FakeElement('div');
global._decorateTransparentEventRow = node => node;
global._anchorSceneToolCallFromRow = () => ({{}});
global.buildToolCard = () => new FakeElement('div');
global._thinkingActivityNode = () => new FakeElement('div');
eval(extractFunc('_anchorSceneProseMatchesFinalAnswer'));
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
const liveRow = {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: 'live-prose:stream-1:near',
text: {json.dumps(prefix_text)},
}};
const liveResult = _anchorSceneTransparentNodeForRow(liveRow, {{
settled: true,
finalAnswer: {json.dumps(final_answer)},
liveTokenFinalPrefixEligible: true,
}});
process.stdout.write(JSON.stringify(liveResult === null));
"""
data = _run_node(UI_JS, script, tmp_path)
assert data is True
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_render_fallback_preserves_pre_tool_live_token_prefix_rows(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(UI_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
class FakeElement {{
constructor(tag) {{
this.tagName = String(tag || 'div').toUpperCase();
this.attributes = Object.create(null);
this.dataset = Object.create(null);
}}
setAttribute(name, value) {{
this.attributes[name] = String(value);
if (name.startsWith('data-')) {{
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
this.dataset[key] = String(value);
}}
}}
}}
global.window = {{}};
global.document = {{ createElement(tag) {{ return new FakeElement(tag); }} }};
global._anchorSceneNodeForRow = () => new FakeElement('div');
global._decorateTransparentEventRow = node => node;
global._anchorSceneToolCallFromRow = () => ({{}});
global.buildToolCard = () => new FakeElement('div');
global._thinkingActivityNode = () => new FakeElement('div');
eval(extractFunc('_anchorSceneProseMatchesFinalAnswer'));
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
const liveRow = {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: {json.dumps(fixture_row["local_id"])},
text: {json.dumps(prefix_text)},
}};
const liveResult = _anchorSceneTransparentNodeForRow(liveRow, {{
settled: true,
finalAnswer: {json.dumps(final_answer)},
liveTokenFinalPrefixEligible: false,
}});
process.stdout.write(JSON.stringify({{
visible: !!liveResult,
rowId: liveResult && liveResult.attributes && liveResult.attributes['data-anchor-row-id'] || '',
}}));
"""
data = _run_node(UI_JS, script, tmp_path)
assert data == {"visible": True, "rowId": fixture_row["local_id"]}
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_non_live_prefix_rows_survive_mode_and_attachment_variants(tmp_path):
fixture_scene, fixture_row = _issue5749_captured_scene()
final_answer = fixture_scene["final_answer"]
prefix_text = fixture_row["text"]
script = f"""
const src = {json.dumps(UI_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
class FakeElement {{
constructor(tag) {{
this.tagName = String(tag || 'div').toUpperCase();
this.attributes = Object.create(null);
this.dataset = Object.create(null);
}}
setAttribute(name, value) {{
this.attributes[name] = String(value);
if (name.startsWith('data-')) {{
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
this.dataset[key] = String(value);
}}
}}
}}
global.window = {{}};
global.document = {{ createElement(tag) {{ return new FakeElement(tag); }} }};
global._anchorSceneNodeForRow = () => new FakeElement('div');
global._decorateTransparentEventRow = node => node;
global._anchorSceneToolCallFromRow = () => ({{}});
global.buildToolCard = () => new FakeElement('div');
global._thinkingActivityNode = () => new FakeElement('div');
eval(extractFunc('_anchorSceneProseMatchesFinalAnswer'));
eval(extractFunc('_anchorSceneLiveTokenFinalPrefix'));
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
const variants = [
{{
label: 'full_dom',
row: {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'manual',
local_id: 'session-prose:stream-1:3',
text: {json.dumps(prefix_text)},
attachments: [{{ id: 'attachment-1' }}],
}},
opts: {{ settled: true, finalAnswer: {json.dumps(final_answer)} }},
}},
{{
label: 'virtualized',
row: {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'manual',
local_id: 'session-prose:stream-1:4',
text: {json.dumps(prefix_text)},
}},
opts: {{ settled: false, finalAnswer: {json.dumps(final_answer)} }},
}},
{{
label: 'attachments',
row: {{
role: 'prose',
kind: 'process_prose',
source_event_type: 'manual',
local_id: 'session-prose:stream-1:5',
text: {json.dumps(prefix_text)},
attachments: [{{ id: 'attachment-2' }}],
}},
opts: {{ settled: true, finalAnswer: {json.dumps(final_answer)} }},
}},
];
const rendered = variants.map(({{
label,
row,
opts,
}}) => {{
const node = _anchorSceneTransparentNodeForRow(row, opts);
return {{
label,
visible: !!node,
rowId: node && node.attributes && node.attributes['data-anchor-row-id'] || '',
attachmentCount: Array.isArray(row.attachments) ? row.attachments.length : 0,
}};
}});
process.stdout.write(JSON.stringify(rendered));
"""
data = _run_node(UI_JS, script, tmp_path)
assert data == [
{"label": "full_dom", "visible": True, "rowId": "session-prose:stream-1:3", "attachmentCount": 1},
{"label": "virtualized", "visible": True, "rowId": "session-prose:stream-1:4", "attachmentCount": 0},
{"label": "attachments", "visible": True, "rowId": "session-prose:stream-1:5", "attachmentCount": 1},
]
@pytest.mark.reproduction
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_issue5749_multi_segment_settled_tool_rows_do_not_shield_final_accumulator(tmp_path):
"""#5758 gap: in a multi-segment turn the settled per-message tool rows are
appended AFTER the projected live rows, so a combined-list "after the last
tool row" boundary never marked the final segment's live-prose accumulator
as final — its stale prefix snapshot survived settlement and rendered as a
duplicate of the answer's beginning above the settled segment. The boundary
must come from the live projection's own chronology: the accumulator (after
the last PROJECTED tool row) is dropped, while pre-tool narration and every
tool row survive."""
final_answer = (
"## Verdict\n\nThe report is mostly right but oversimplifies: curated skill "
"bundles help strongly across all tested harness combinations, while "
"one-shot self-generated bundles without quality control regress the "
"baseline on every configuration we measured. The correct takeaway is to "
"keep a curated, versioned library instead of disabling skills entirely."
)
accumulator_prefix = final_answer[:70]
narration_text = "Checking the local skill inventory before judging the claim."
script = f"""
const src = {json.dumps(MESSAGES_JS)};
function extractFunc(name) {{
const start = src.indexOf('function ' + name);
if (start === -1) throw new Error(name + ' not found');
const params = src.indexOf('(', start);
let depth = 0, close = -1;
for (let i = params; i < src.length; i++) {{
if (src[i] === '(') depth++;
else if (src[i] === ')') {{
depth--;
if (depth === 0) {{ close = i; break; }}
}}
}}
const brace = src.indexOf('{{', close);
depth = 0;
for (let i = brace; i < src.length; i++) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') {{
depth--;
if (depth === 0) return src.slice(start, i + 1);
}}
}}
throw new Error(name + ' body did not close');
}}
global.window = {{
chatActivityMode() {{ return 'transparent_stream'; }},
_chatActivityDisplayMode: 'transparent_stream',
_transparentStream: true,
}};
global.S = {{ session: {{}} }};
eval(extractFunc('_anchorSceneCleanText'));
eval(extractFunc('_anchorSceneTextKey'));
eval(extractFunc('_anchorSceneExistingRowKey'));
eval(extractFunc('_anchorSceneRowHasLiveIdentity'));
eval(extractFunc('_anchorSceneSettleLiveRunningRow'));
eval(extractFunc('_anchorSceneRowLooksLikeFinalAnswer'));
eval(extractFunc('_anchorSceneRowTextOverlapsExisting'));
eval(extractFunc('_anchorSceneMessageRowsHaveThinking'));
eval(extractFunc('_completeSettledAnchorSceneForTurn'));
function _anchorSceneActiveMode() {{ return 'transparent_stream'; }}
function _anchorSceneFinalAnswerText(message) {{ return message && (message.final_answer || message.content || ''); }}
// The settled per-message rows re-list the turn's tool as a completed copy;
// it lands AFTER the projected live rows in the combined ordering.
function _anchorSceneRowsByMessageIndex() {{
return new Map([[2, [{{
role: 'tool',
kind: 'tool_result',
source_event_type: 'tool_complete',
local_id: 'settled-tool-row-1',
tool_call_id: 'call-1',
text: '',
status: 'completed',
}}]]]);
}}
function _anchorSceneMessageRef(message) {{ return String(message && message.id || ''); }}
function _anchorSceneTurnDurationForSettlement() {{ return 0; }}
function _anchorSceneRowDisplayHintForMode(row, sceneMode) {{
const hints = row && typeof row === 'object' && row.display_hints && typeof row.display_hints === 'object' ? row.display_hints : null;
if (sceneMode === 'transparent_stream') return (hints && hints.transparent_stream) || 'chronological_activity';
if (sceneMode === 'compact_worklog') return (hints && hints.compact_worklog) || row && row.display_hint || 'activity_row';
return row && row.display_hint || 'activity_row';
}}
const messages = [
{{ role: 'user', content: 'Prompt', id: 'user-1' }},
{{ role: 'assistant', content: {json.dumps(narration_text)}, id: 'assistant-1' }},
{{ role: 'assistant', content: {json.dumps(final_answer)}, id: 'assistant-2' }},
];
const scene = _completeSettledAnchorSceneForTurn(messages, 2, {{
mode: 'transparent_stream',
final_answer: {json.dumps(final_answer)},
lifecycle: {{ terminal_state: 'done' }},
identity: {{ source_message_refs: ['legacy'] }},
activity_rows: [
{{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: 'live-prose:stream-multi:1',
text: {json.dumps(narration_text)},
status: 'running',
}},
{{
role: 'tool',
kind: 'tool_started',
source_event_type: 'tool',
local_id: 'live-tool-row-1',
tool_call_id: 'call-1',
text: '',
status: 'running',
}},
{{
role: 'prose',
kind: 'process_prose',
source_event_type: 'token',
local_id: 'live-prose:stream-multi:2',
text: {json.dumps(accumulator_prefix)},
status: 'running',
}},
],
}});
process.stdout.write(JSON.stringify(scene.activity_rows.map(row => ({{
role: row.role,
local_id: row.local_id,
text: row.text || '',
}}))));
"""
data = _run_node(MESSAGES_JS, script, tmp_path)
local_ids = [row["local_id"] for row in data]
# The stale accumulator snapshot (strict prefix of the final answer, well
# under the 0.9 near-match ratio) is dropped; narration and tools survive.
assert "live-prose:stream-multi:2" not in local_ids
assert "live-prose:stream-multi:1" in local_ids
assert data[0]["text"] == narration_text
assert any(row["role"] == "tool" for row in data)