mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-08 16:52:02 +00:00
Gate settled anchor fallbacks by raw index
This commit is contained in:
@@ -309,9 +309,13 @@ must not compete with an anchor-owned turn.
|
||||
| Historical / non-anchor transcripts | No anchor owner is available. | Raw `content[]`, persisted `session.tool_calls`, role=`tool` rows, and partial tool-call snapshots continue to recover visible tool/prose history. | None. These paths remain until replay/runtime-journal coverage proves the same transcript shapes hydrate through anchors. |
|
||||
| Live reattach / session switch | `renderLiveAnchorActivityScene()` consumes the projected live scene, and session switch first attempts runtime-journal anchor scene restore. | Saved live DOM snapshots and `INFLIGHT` tool replay. | Snapshot fallback only runs when no usable live anchor scene can be rendered. |
|
||||
|
||||
This matrix is an audit baseline, not permission to delete fallbacks. Fallback
|
||||
removal should wait for replay/runtime-journal parity tests that prove all
|
||||
supported settled transcript sources hydrate through anchors.
|
||||
This matrix is the current settled-render contract, backed by runtime-journal
|
||||
hydration parity and behavior-level renderer ownership coverage. Fallback paths
|
||||
are compatibility-only rebuilds for historical or genuinely non-anchor
|
||||
transcripts, and the renderer must gate them by explicit raw transcript indexes
|
||||
so object identity or duplicate message references cannot let fallback rows
|
||||
compete with an anchor-owned turn. Removing a compatibility path still requires
|
||||
new evidence that the supported transcript shape hydrates through anchors.
|
||||
|
||||
## Source Event Classification
|
||||
|
||||
|
||||
+11
-10
@@ -11773,6 +11773,14 @@ function _transparentStreamOrderedParts(message){
|
||||
}
|
||||
return hasText&&hasTool?ordered:null;
|
||||
}
|
||||
function _legacySettledFallbackHasToolMetadata(message){
|
||||
if(!message||message.role!=='assistant'||message._anchor_activity_scene) return false;
|
||||
return !!(
|
||||
(Array.isArray(message.tool_calls)&&message.tool_calls.length>0)||
|
||||
(Array.isArray(message._partial_tool_calls)&&message._partial_tool_calls.length>0)||
|
||||
(Array.isArray(message.content)&&message.content.some(part=>part&&typeof part==='object'&&part.type==='tool_use'))
|
||||
);
|
||||
}
|
||||
function _transparentOrderedDisplayText(text){
|
||||
return _stripWorkspaceDisplayPrefix(
|
||||
_stripAttachedFilesMarkerForDisplay(
|
||||
@@ -12614,12 +12622,8 @@ function renderMessages(options){
|
||||
// tracking, or runs that didn't go through the normal streaming path), build
|
||||
// a display list from per-message tool_calls (OpenAI format) stored in each
|
||||
// assistant message. This covers the reload case described in issue #140.
|
||||
const hasMessageToolMetadata=!S.busy&&Array.isArray(S.messages)&&S.messages.some(m=>
|
||||
m&&m.role==='assistant'&&!anchorOwnedAssistantRawIdxs.has(S.messages.indexOf(m))&&(
|
||||
(Array.isArray(m.tool_calls)&&m.tool_calls.length>0)||
|
||||
(Array.isArray(m._partial_tool_calls)&&m._partial_tool_calls.length>0)||
|
||||
(Array.isArray(m.content)&&m.content.some(p=>p&&typeof p==='object'&&p.type==='tool_use'))
|
||||
)
|
||||
const hasMessageToolMetadata=!S.busy&&Array.isArray(S.messages)&&S.messages.some((m,rawIdx)=>
|
||||
!anchorOwnedAssistantRawIdxs.has(rawIdx)&&_legacySettledFallbackHasToolMetadata(m)
|
||||
);
|
||||
if(!S.busy && (hasMessageToolMetadata||!S.toolCalls||!S.toolCalls.length)){
|
||||
// Index tool outputs by tool_call_id / tool_use_id so the
|
||||
@@ -12664,10 +12668,7 @@ function renderMessages(options){
|
||||
}
|
||||
if(m.role==='assistant'){
|
||||
if(anchorOwnedAssistantRawIdxs.has(rawIdx)) return;
|
||||
const hasTopLevelToolCalls=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
|
||||
const hasPartialToolCalls=Array.isArray(m._partial_tool_calls)&&m._partial_tool_calls.length>0;
|
||||
const hasContentToolUse=Array.isArray(m.content)&&m.content.some(p=>p&&typeof p==='object'&&p.type==='tool_use');
|
||||
if(hasTopLevelToolCalls||hasContentToolUse||hasPartialToolCalls) fallbackToolSources.push({m,rawIdx});
|
||||
if(_legacySettledFallbackHasToolMetadata(m)) fallbackToolSources.push({m,rawIdx});
|
||||
}
|
||||
});
|
||||
const derived=[];
|
||||
|
||||
@@ -264,7 +264,9 @@ def test_phase0_doc_records_settled_fallback_ownership_matrix():
|
||||
assert "| Settled Compact Worklog activity |" in doc
|
||||
assert "| Settled Transparent Stream activity |" in doc
|
||||
assert "| Historical / non-anchor transcripts |" in doc
|
||||
assert "This matrix is an audit baseline, not permission to delete fallbacks." in doc
|
||||
assert "This matrix is the current settled-render contract" in doc
|
||||
assert "compatibility-only rebuilds" in doc
|
||||
assert "explicit raw transcript indexes" in doc
|
||||
|
||||
|
||||
def test_function_extractor_handles_nested_template_literal_interpolation():
|
||||
@@ -404,6 +406,9 @@ def test_render_messages_keeps_anchor_owned_turn_out_of_legacy_activity_rebuilds
|
||||
|
||||
render_source = _function_source(_ui_js(), "renderMessages")
|
||||
transparent_source = _function_source(_ui_js(), "_transparentStreamOrderedParts")
|
||||
legacy_metadata_source = _function_source(
|
||||
_ui_js(), "_legacySettledFallbackHasToolMetadata"
|
||||
)
|
||||
script = textwrap.dedent(
|
||||
f"""
|
||||
class FakeClassList {{
|
||||
@@ -677,6 +682,7 @@ def test_render_messages_keeps_anchor_owned_turn_out_of_legacy_activity_rebuilds
|
||||
}}
|
||||
|
||||
eval({json.dumps(transparent_source)});
|
||||
eval({json.dumps(legacy_metadata_source)});
|
||||
eval({json.dumps(render_source)});
|
||||
|
||||
const toolResult = {{ role: 'tool', tool_call_id: 'toolu_1', content: 'tool result' }};
|
||||
@@ -767,11 +773,58 @@ def test_render_messages_keeps_anchor_owned_turn_out_of_legacy_activity_rebuilds
|
||||
sToolCalls: S.toolCalls.length,
|
||||
}};
|
||||
|
||||
elements.msgInner = new FakeElement('div');
|
||||
legacyCards = [];
|
||||
const duplicateAnchorTool = {{ type: 'tool_use', id: 'toolu_anchor_dup', name: 'terminal', input: {{ cmd: 'anchor' }} }};
|
||||
const duplicateHistoricalTool = {{ type: 'tool_use', id: 'toolu_hist_dup', name: 'terminal', input: {{ cmd: 'history' }} }};
|
||||
const duplicateAnchorCall = {{
|
||||
id: 'toolu_anchor_dup',
|
||||
function: {{ name: 'terminal', arguments: '{{"cmd":"anchor"}}' }},
|
||||
}};
|
||||
const duplicateAnchorOwned = {{
|
||||
role: 'assistant',
|
||||
content: [{{ type: 'text', text: 'Duplicate answer' }}, duplicateAnchorTool],
|
||||
tool_calls: [duplicateAnchorCall],
|
||||
_anchor_activity_scene: {{
|
||||
version: 'activity_scene_v1',
|
||||
activity_rows: [{{ id: 'row-dup', kind: 'tool', role: 'tool', tool: {{ name: 'terminal' }} }}],
|
||||
final_answer: 'Duplicate answer',
|
||||
}},
|
||||
}};
|
||||
const duplicateHistorical = {{
|
||||
role: 'assistant',
|
||||
content: [{{ type: 'text', text: 'Duplicate answer' }}, duplicateHistoricalTool],
|
||||
}};
|
||||
S = {{
|
||||
session: {{
|
||||
session_id: 's4',
|
||||
tool_calls: [{{ tid: 'toolu_hist_dup', snippet: 'historical persisted result' }}],
|
||||
}},
|
||||
messages: [
|
||||
{{ role: 'user', content: 'anchor turn' }},
|
||||
duplicateAnchorOwned,
|
||||
{{ role: 'user', content: 'historical turn' }},
|
||||
duplicateHistorical,
|
||||
toolResult,
|
||||
],
|
||||
toolCalls: [{{ tid: 'toolu_anchor_dup', assistant_msg_idx: 1, name: 'terminal', snippet: 'anchor session fallback' }}],
|
||||
busy: false,
|
||||
}};
|
||||
renderMessages();
|
||||
const duplicateReferenceSummary = {{
|
||||
anchorGroups: elements.msgInner.querySelectorAll('[data-anchor-settled-scene-owner]').length,
|
||||
legacyGroups: elements.msgInner.querySelectorAll('[data-legacy-fallback-owner]').length,
|
||||
legacyRows: elements.msgInner.querySelectorAll('.tool-card-row').length,
|
||||
legacyCards,
|
||||
sToolCalls: S.toolCalls.length,
|
||||
}};
|
||||
|
||||
console.log(JSON.stringify({{
|
||||
selectorSanity,
|
||||
anchorSummary,
|
||||
historicalSummary,
|
||||
rawHistoricalSummary,
|
||||
duplicateReferenceSummary,
|
||||
}}));
|
||||
"""
|
||||
)
|
||||
@@ -810,6 +863,15 @@ def test_render_messages_keeps_anchor_owned_turn_out_of_legacy_activity_rebuilds
|
||||
assert raw_snippets["partial_1"] == "partial result"
|
||||
assert raw_snippets["content_1"] == "persisted content result"
|
||||
|
||||
duplicate_cards = result["duplicateReferenceSummary"]["legacyCards"]
|
||||
assert result["duplicateReferenceSummary"]["anchorGroups"] == 1
|
||||
assert result["duplicateReferenceSummary"]["legacyGroups"] == 1
|
||||
assert result["duplicateReferenceSummary"]["legacyRows"] >= 1
|
||||
assert result["duplicateReferenceSummary"]["sToolCalls"] >= 1
|
||||
assert {card["tid"] for card in duplicate_cards} == {"toolu_hist_dup"}
|
||||
assert "toolu_anchor_dup" not in {card["tid"] for card in duplicate_cards}
|
||||
assert duplicate_cards[0]["snippet"] == "historical persisted result"
|
||||
|
||||
|
||||
def test_settled_legacy_tool_rebuild_excludes_anchor_owned_turns():
|
||||
render = _function_body(_ui_js(), "renderMessages")
|
||||
@@ -821,7 +883,9 @@ def test_settled_legacy_tool_rebuild_excludes_anchor_owned_turns():
|
||||
source_collect = render.index("fallbackToolSources.push({m,rawIdx});")
|
||||
|
||||
assert set_decl < collect_segments < metadata_scan < fallback_sources < source_collect
|
||||
assert "!anchorOwnedAssistantRawIdxs.has(S.messages.indexOf(m))" in render
|
||||
assert "S.messages.indexOf(m)" not in render
|
||||
assert "S.messages.some((m,rawIdx)=>" in render
|
||||
assert "!anchorOwnedAssistantRawIdxs.has(rawIdx)&&_legacySettledFallbackHasToolMetadata(m)" in render
|
||||
assert "if(anchorOwnedAssistantRawIdxs.has(rawIdx)) return;" in render
|
||||
|
||||
|
||||
|
||||
@@ -86,8 +86,9 @@ def test_rendermessages_treats_partial_toolcall_assistants_as_visible():
|
||||
|
||||
def test_rendermessages_rebuilds_tool_cards_from_partial_tool_calls():
|
||||
"""Fallback reconstruction should include private `_partial_tool_calls` rows."""
|
||||
assert "const hasPartialToolCalls=Array.isArray(m._partial_tool_calls)&&m._partial_tool_calls.length>0;" in UI_JS
|
||||
assert "if(hasTopLevelToolCalls||hasContentToolUse||hasPartialToolCalls) fallbackToolSources.push({m,rawIdx});" in UI_JS
|
||||
assert "function _legacySettledFallbackHasToolMetadata(message)" in UI_JS
|
||||
assert "Array.isArray(message._partial_tool_calls)&&message._partial_tool_calls.length>0" in UI_JS
|
||||
assert "if(_legacySettledFallbackHasToolMetadata(m)) fallbackToolSources.push({m,rawIdx});" in UI_JS
|
||||
assert "if(Array.isArray(m._partial_tool_calls)){" in UI_JS
|
||||
assert "tc.snippet||tc.result||tc.output||tc.preview" in UI_JS
|
||||
assert "done:true" in UI_JS
|
||||
|
||||
@@ -771,7 +771,12 @@ def test_anchor_owned_settled_turn_skips_legacy_worklog_rebuild():
|
||||
assert "anchorOwnedAssistantRawIdxs.add(idx)" in render
|
||||
assert "if(anchorOwnedAssistantRawIdxs.has(aIdx)) continue;" in render
|
||||
assert "if(anchorOwnedAssistantRawIdxs.has(rawIdx)) return;" in render
|
||||
assert "!anchorOwnedAssistantRawIdxs.has(S.messages.indexOf(m))" in render
|
||||
assert "S.messages.indexOf(m)" not in render
|
||||
assert "S.messages.some((m,rawIdx)=>" in render
|
||||
assert (
|
||||
"!anchorOwnedAssistantRawIdxs.has(rawIdx)"
|
||||
"&&_legacySettledFallbackHasToolMetadata(m)"
|
||||
) in render
|
||||
|
||||
|
||||
def test_transparent_stream_renders_persisted_anchor_scene_after_reload():
|
||||
|
||||
Reference in New Issue
Block a user