Gate settled anchor fallbacks by raw index

This commit is contained in:
Frank Song
2026-06-30 07:54:52 +08:00
parent dd4d4b2e90
commit eff01bf322
5 changed files with 93 additions and 18 deletions
@@ -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
View File
@@ -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=[];
+66 -2
View File
@@ -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
+3 -2
View File
@@ -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():