diff --git a/docs/architecture/stable-assistant-turn-anchor-phase0.md b/docs/architecture/stable-assistant-turn-anchor-phase0.md index f75d81e4..d6c8e431 100644 --- a/docs/architecture/stable-assistant-turn-anchor-phase0.md +++ b/docs/architecture/stable-assistant-turn-anchor-phase0.md @@ -23,10 +23,13 @@ streaming or rendering yet. pinned by tests before visible wiring begins. - Slice 4 starts RFC Phase 3 by routing settled assistant final prose through the anchor owner before `renderMessages()` renders the final assistant body. -- The next independently reviewable boundary is activity/render-scene projection - for reasoning, tool rows, and transparent-stream/worklog metadata. `S.messages`, - `INFLIGHT`, stream-local state, and DOM nodes remain projection/cache layers - outside the settled final-prose path. +- Slice 5 starts RFC Phase 5 by projecting anchor-owned activity events into a + renderer-neutral activity scene that Compact Worklog and Transparent Stream + can later consume from the same ordered rows. +- The next independently reviewable boundary is wiring one current renderer to + the activity scene. `S.messages`, `INFLIGHT`, stream-local state, and DOM nodes + remain projection/cache layers outside the settled final-prose path and the + inert activity-scene projection. ## State Layers @@ -136,6 +139,22 @@ This is intentionally narrower than render-scene ownership: live stream tokens, replay hydration, worklog rows, transparent-stream rows, tool cards, `INFLIGHT`, and DOM continuity are still not consumed by the anchor registry in this slice. +## Slice 5 Activity Scene Projection + +`HermesAssistantTurnAnchors.projectAssistantTurnAnchorActivityScene()` projects +an anchor or registry into `activity_scene_v1`: identity, lifecycle, +`final_answer`, `final_message_ref`, terminal state, and an ordered +`activity_rows` list. + +The rows are renderer-neutral. Compact Worklog receives display hints such as +`main_prose`, `collapsed_thinking`, `tool_row`, and `terminal_status_row`. +Transparent Stream receives the same row IDs, order, kinds, roles, text, tool +IDs, and sanitized payloads with a chronological display hint. This pins the +shared input shape before either renderer is rewired. + +This slice is still inert. No current UI module consumes the activity scene. +`renderMessages()` and the live streaming hot path are unchanged by this slice. + ## Source Event Classification Phase 0 classifies current sources before changing render behavior: diff --git a/static/assistant_turn_anchors.js b/static/assistant_turn_anchors.js index 21d6af6d..4edf3422 100644 --- a/static/assistant_turn_anchors.js +++ b/static/assistant_turn_anchors.js @@ -786,6 +786,262 @@ }); } + function _anchorFromProjectionInput(input){ + if(!input||typeof input!=='object') return null; + if(input.anchor&&typeof input.anchor==='object') return input.anchor; + if(input.identity&&typeof input.identity==='object') return input; + return null; + } + + function _activityRowId(event, index){ + const eventId=_cleanString(_own(event,'event_id')); + if(eventId) return eventId; + const runId=_cleanString(_own(event,'run_id')); + const seq=_own(event,'seq'); + if(runId&&seq!==undefined&&seq!==null&&seq!=='') return [runId,String(seq)].join(':'); + const localId=_cleanString(_own(event,'local_id')); + if(localId){ + const sourceType=_cleanString(_own(event,'source_event_type'))||_cleanString(_own(event,'kind'))||'event'; + return [localId,sourceType,String(index)].join(':'); + } + return 'activity:'+String(index); + } + + function _activityRowText(event){ + const payload=_own(event,'payload')||{}; + return _firstTextValue( + _own(payload,'text'), + _own(payload,'content'), + _own(payload,'message'), + _own(payload,'summary'), + _own(payload,'result'), + _own(payload,'output') + ); + } + + function _isToolActivityKind(kind){ + return kind==='tool_started'||kind==='tool_updated'||kind==='tool_completed'; + } + + function _activityRowToolId(event, kind){ + if(!_isToolActivityKind(kind)) return null; + const payload=_own(event,'payload')||{}; + return _firstTextValue( + _own(payload,'tool_call_id'), + _own(payload,'tool_use_id'), + _own(payload,'call_id'), + _own(payload,'tid'), + _own(payload,'id') + )||null; + } + + function _activityPayloadFirst(payload, keys){ + return _firstOwn(payload||{},keys); + } + + function _activityRowToolDone(kind, status, payload){ + if(payload&&typeof _own(payload,'done')==='boolean') return _own(payload,'done'); + if(kind==='tool_completed') return true; + if(kind==='tool_started'||kind==='tool_updated') return false; + if(status==='completed'||status==='error'||status==='failed') return true; + if(status==='running'||status==='pending') return false; + return null; + } + + function _activityRowToolIsError(status, payload){ + if(payload&&typeof _own(payload,'is_error')==='boolean') return _own(payload,'is_error'); + const raw=_cleanString(status).toLowerCase(); + if(raw==='error'||raw==='failed'||raw==='failure') return true; + return false; + } + + function _activityRowGroup(event, payload, index){ + const activitySegmentSeq=_activityPayloadFirst(payload,['activitySegmentSeq','activity_segment_seq','segmentSeq','segment_seq']); + const activityBurstId=_activityPayloadFirst(payload,['activityBurstId','activity_burst_id','burstId','burst_id']); + const assistantMsgIdx=_activityPayloadFirst(payload,['assistant_msg_idx','assistantMessageIndex','assistant_msg_index']); + const cleanSegment=activitySegmentSeq!==undefined&&activitySegmentSeq!==null&&String(activitySegmentSeq)!=='' + ? activitySegmentSeq + : null; + const cleanBurst=activityBurstId!==undefined&&activityBurstId!==null&&String(activityBurstId)!=='' + ? activityBurstId + : null; + const cleanAssistant=assistantMsgIdx!==undefined&&assistantMsgIdx!==null&&String(assistantMsgIdx)!=='' + ? assistantMsgIdx + : null; + const fallbackSeq=_own(event,'seq'); + const fallbackKey=fallbackSeq!==undefined&&fallbackSeq!==null&&fallbackSeq!==''?`event:${String(fallbackSeq)}`:`activity:${String(index)}`; + const groupKey=cleanSegment!==null + ? `segment:${String(cleanSegment)}` + : cleanBurst!==null + ? `burst:${String(cleanBurst)}` + : cleanAssistant!==null + ? `assistant:${String(cleanAssistant)}` + : fallbackKey; + return Object.freeze({ + group_key:groupKey, + activity_burst_id:cleanBurst, + activity_segment_seq:cleanSegment, + assistant_msg_idx:cleanAssistant, + }); + } + + function _activityRowThinking(event, kind, text){ + if(kind!=='reasoning') return null; + const payload=_own(event,'payload')||{}; + const thinkingText=_firstTextValue( + _own(payload,'thinking'), + _own(payload,'reasoning'), + _own(payload,'text'), + text + ); + const preview=thinkingText?String(thinkingText).replace(/\s+/g,' ').trim():''; + return Object.freeze({ + text:thinkingText||'', + preview:preview.length>180?`${preview.slice(0,177)}...`:preview, + dedupe_key:preview?`thinking:${preview.toLowerCase()}`:'', + }); + } + + function _activityRowTool(event, kind, status, text, toolCallId){ + if(!_isToolActivityKind(kind)) return null; + const payload=_own(event,'payload')||{}; + const toolName=_cleanString( + _activityPayloadFirst(payload,['name','tool_name','function_name'])|| + (_own(payload,'function')&&_own(_own(payload,'function'),'name')) + )||'tool'; + const args=_activityPayloadFirst(payload,['args','arguments','input','params']); + const preview=_firstTextValue( + _own(payload,'preview'), + _own(payload,'summary'), + text + ); + const snippet=_firstTextValue( + _own(payload,'snippet'), + _own(payload,'result'), + _own(payload,'output') + ); + const done=_activityRowToolDone(kind,status,payload); + const isError=_activityRowToolIsError(status,payload); + const signatureParts=[ + toolName, + toolCallId||'', + JSON.stringify(_sanitizePayload(args||{})), + ]; + return Object.freeze({ + id:toolCallId, + name:toolName, + args:_sanitizePayload(args||{}), + preview:preview||'', + snippet:snippet||'', + result:_sanitizePayload(_own(payload,'result'))??null, + output:_sanitizePayload(_own(payload,'output'))??null, + done, + is_error:isError, + duration:_activityPayloadFirst(payload,['duration','duration_seconds','elapsed'])??null, + started_at:_activityPayloadFirst(payload,['started_at','startedAt'])??null, + signature:signatureParts.join('|'), + }); + } + + function _activityRowRole(kind){ + if(kind==='process_prose') return 'prose'; + if(kind==='reasoning') return 'thinking'; + if(_isToolActivityKind(kind)) return 'tool'; + if(kind==='lifecycle_status') return 'lifecycle'; + if(kind==='control_boundary') return 'control'; + if(kind==='terminal_status') return 'terminal'; + return 'activity'; + } + + function _activityRowDisplayHint(kind, mode){ + if(mode==='transparent_stream') return 'chronological_activity'; + if(kind==='process_prose') return 'main_prose'; + if(kind==='reasoning') return 'collapsed_thinking'; + if(_isToolActivityKind(kind)) return 'tool_row'; + if(kind==='lifecycle_status') return 'quiet_lifecycle_row'; + if(kind==='control_boundary') return 'control_boundary_row'; + if(kind==='terminal_status') return 'terminal_status_row'; + return 'activity_row'; + } + + function _activityRowDisplayHints(kind){ + return Object.freeze({ + compact_worklog:_activityRowDisplayHint(kind,'compact_worklog'), + transparent_stream:_activityRowDisplayHint(kind,'transparent_stream'), + }); + } + + function _activitySceneRow(event, index, mode){ + const payload=_own(event,'payload'); + const kind=_cleanString(_own(event,'kind'))||'activity'; + const status=_cleanString(_own(event,'status'))||null; + const text=_activityRowText(event); + const toolCallId=_activityRowToolId(event,kind); + const sanitizedPayload=_sanitizePayload(payload); + return Object.freeze({ + row_id:_activityRowId(event,index), + order_index:index, + kind, + role:_activityRowRole(kind), + display_hint:_activityRowDisplayHint(kind,mode), + display_hints:_activityRowDisplayHints(kind), + source_event_type:_cleanString(_own(event,'source_event_type'))||null, + event_id:_cleanString(_own(event,'event_id'))||null, + local_id:_cleanString(_own(event,'local_id'))||null, + run_id:_cleanString(_own(event,'run_id'))||null, + stream_id:_cleanString(_own(event,'stream_id'))||null, + seq:_own(event,'seq')??null, + status, + created_at:_own(event,'created_at')??null, + identity:Object.freeze({ + event_id:_cleanString(_own(event,'event_id'))||null, + local_id:_cleanString(_own(event,'local_id'))||null, + run_id:_cleanString(_own(event,'run_id'))||null, + stream_id:_cleanString(_own(event,'stream_id'))||null, + seq:_own(event,'seq')??null, + }), + group:_activityRowGroup(event,payload||{},index), + text, + thinking:_activityRowThinking(event,kind,text), + tool_call_id:toolCallId, + tool:_activityRowTool(event,kind,status,text,toolCallId), + payload:sanitizedPayload, + }); + } + + function projectAssistantTurnAnchorActivityScene(input, options){ + const anchor=_anchorFromProjectionInput(input); + const opts=(options&&typeof options==='object')?options:{}; + const requestedMode=_cleanString(_own(opts,'mode')); + const mode=requestedMode==='transparent_stream'?'transparent_stream':'compact_worklog'; + if(!anchor){ + return Object.freeze({ + version:'activity_scene_v1', + mode, + identity:Object.freeze({source_message_refs:Object.freeze([])}), + lifecycle:Object.freeze({}), + final_answer:'', + final_message_ref:null, + terminal_state:null, + activity_rows:Object.freeze([]), + }); + } + const rows=(Array.isArray(anchor.activity_events)?anchor.activity_events:[]) + .map((event,index)=>_activitySceneRow(event,index,mode)); + const lifecycle=_copyObject(anchor.lifecycle); + const content=anchor.content&&typeof anchor.content==='object'?anchor.content:{}; + return Object.freeze({ + version:'activity_scene_v1', + mode, + identity:_frozenIdentityCopy(anchor.identity||{}), + lifecycle:Object.freeze(lifecycle), + final_answer:typeof content.final_answer==='string'?content.final_answer:'', + final_message_ref:typeof content.final_message_ref==='string'?content.final_message_ref:null, + terminal_state:_cleanString(_own(lifecycle,'terminal_state'))||null, + activity_rows:Object.freeze(rows), + }); + } + function createAssistantTurnAnchorSeed(input){ const opts=(input&&typeof input==='object')?input:{}; const sessionId=_cleanString(opts.session_id); @@ -845,7 +1101,7 @@ } ROOT.HermesAssistantTurnAnchors=Object.freeze({ - version:'slice4-final-projection', + version:'slice5-activity-scene', activityEventKinds:ACTIVITY_EVENT_KINDS, stateLayers:STATE_LAYERS, sourceEventClassification:SOURCE_EVENT_CLASSIFICATION, @@ -863,6 +1119,7 @@ applyAssistantTurnAnchorSourceEvents, createAssistantTurnAnchorShadowSnapshot, projectAssistantTurnAnchorSettledMessageFinalAnswer, + projectAssistantTurnAnchorActivityScene, isAssistantTurnAnchorActivityKind, }); })(); diff --git a/tests/test_stable_assistant_turn_anchor_normalizer.py b/tests/test_stable_assistant_turn_anchor_normalizer.py index 1ab6234c..1143977d 100644 --- a/tests/test_stable_assistant_turn_anchor_normalizer.py +++ b/tests/test_stable_assistant_turn_anchor_normalizer.py @@ -182,7 +182,7 @@ def test_normalizer_maps_live_and_replay_to_same_anchor_event_identity(): live = data["liveToken"] replay = data["replayToken"] - assert data["version"] == "slice4-final-projection" + assert data["version"] == "slice5-activity-scene" assert live["classification"] == "activity" assert live["dedupe_key"] == 'event_id:"run-1:7"' assert replay["dedupe_key"] == live["dedupe_key"] diff --git a/tests/test_stable_assistant_turn_anchor_registry.py b/tests/test_stable_assistant_turn_anchor_registry.py index f5ae67f8..4be4975d 100644 --- a/tests/test_stable_assistant_turn_anchor_registry.py +++ b/tests/test_stable_assistant_turn_anchor_registry.py @@ -111,6 +111,97 @@ console.log(JSON.stringify({{ assert result.returncode == 0, result.stderr return json.loads(result.stdout) +def _activity_scene_snapshot() -> dict: + assert NODE, "node is required for assistant_turn_anchors.js registry tests" + script = f""" +const fs = require('fs'); +const vm = require('vm'); +const src = fs.readFileSync({json.dumps(str(ANCHORS_JS))}, 'utf8'); +const sandbox = {{window:{{}}}}; +vm.createContext(sandbox); +vm.runInContext(src, sandbox, {{filename:'assistant_turn_anchors.js'}}); +const api = sandbox.window.HermesAssistantTurnAnchors; +const registry = api.createAssistantTurnAnchorRegistry({{ + session_id:'sid-scene', + turn_id:'turn-scene', +}}); +api.applyAssistantTurnAnchorSourceEvents(registry, [ + {{event:'token', payload:{{text:'progress'}}, event_id:'run-scene:1', seq:1}}, + {{event:'reasoning', payload:{{text:'private thinking'}}, event_id:'run-scene:2', seq:2}}, + {{event:'tool', payload:{{ + tool_call_id:'tool-1', + name:'terminal', + args:{{command:'rg anchor static'}}, + preview:'rg anchor static', + activityBurstId:7, + activitySegmentSeq:3, + assistant_msg_idx:12, + started_at:1781200000 + }}, event_id:'run-scene:3', seq:3}}, + {{event:'tool_update', payload:{{ + tool_call_id:'tool-1', + name:'terminal', + text:'running', + preview:'searching workspace', + activityBurstId:7, + activitySegmentSeq:3, + assistant_msg_idx:12 + }}, event_id:'run-scene:4', seq:4}}, + {{event:'tool_complete', payload:{{ + tool_call_id:'tool-1', + name:'terminal', + result:'done', + output:'done', + snippet:'done', + is_error:false, + duration:1.25, + activityBurstId:7, + activitySegmentSeq:3, + assistant_msg_idx:12 + }}, event_id:'run-scene:5', seq:5}}, + {{event:'done', payload:{{}}, event_id:'run-scene:6', seq:6}}, + {{source_type:'settled_message', payload:{{role:'assistant', id:'message-scene', content:'final answer'}}}}, +], {{run_id:'run-scene', stream_id:'stream-scene'}}); +const compact = api.projectAssistantTurnAnchorActivityScene(registry, {{mode:'compact_worklog'}}); +const transparent = api.projectAssistantTurnAnchorActivityScene(registry.anchor, {{mode:'transparent_stream'}}); +const empty = api.projectAssistantTurnAnchorActivityScene(null, {{mode:'transparent_stream'}}); +const seqlessRegistry = api.createAssistantTurnAnchorRegistry({{ + session_id:'sid-seqless', + turn_id:'turn-seqless', +}}); +api.applyAssistantTurnAnchorSourceEvents(seqlessRegistry, [ + {{event:'tool', payload:{{tool_call_id:'tool-same', name:'terminal'}}}}, + {{event:'tool_update', payload:{{tool_call_id:'tool-same', text:'running'}}}}, + {{event:'tool_complete', payload:{{tool_call_id:'tool-same', result:'done'}}}}, +]); +const seqless = api.projectAssistantTurnAnchorActivityScene(seqlessRegistry, {{mode:'compact_worklog'}}); +const zeroRegistry = api.createAssistantTurnAnchorRegistry({{ + session_id:'sid-zero', + turn_id:'turn-zero', +}}); +api.applyAssistantTurnAnchorSourceEvents(zeroRegistry, [ + {{event:'tool', payload:{{ + tool_call_id:'tool-zero', + name:'terminal', + activityBurstId:0, + activitySegmentSeq:0, + assistant_msg_idx:0 + }}, event_id:'run-zero:0', seq:0}}, +]); +const zero = api.projectAssistantTurnAnchorActivityScene(zeroRegistry, {{mode:'compact_worklog'}}); +console.log(JSON.stringify({{ + version:api.version, + compact, + transparent, + empty, + seqless, + zero, +}})); +""" + result = subprocess.run([NODE, "-e", script], text=True, capture_output=True, check=False) + assert result.returncode == 0, result.stderr + return json.loads(result.stdout) + def _final_projection_snapshot() -> dict: assert NODE, "node is required for assistant_turn_anchors.js registry tests" @@ -342,7 +433,7 @@ def test_registry_owns_one_anchor_and_dedupes_live_plus_replay_events(): registry = data["registry"] anchor = registry["anchor"] - assert data["version"] == "slice4-final-projection" + assert data["version"] == "slice5-activity-scene" assert [item["reason"] for item in data["results"][:2]] == [None, "duplicate"] assert registry["event_index"]["dedupe_keys"][:2] == [ 'event_id:"run-1:1"', @@ -442,7 +533,7 @@ def test_registry_does_not_destructively_dedupe_seqless_local_tool_lifecycle(): registry = data["toolRegistry"] anchor = registry["anchor"] - assert data["version"] == "slice4-final-projection" + assert data["version"] == "slice5-activity-scene" assert data["toolResults"] == [ {"applied": True, "reason": None}, {"applied": True, "reason": None}, @@ -492,7 +583,7 @@ def test_shadow_snapshot_feeds_current_source_families_into_one_registry_owner() registry = data["registry"] anchor = registry["anchor"] - assert data["version"] == "slice4-final-projection" + assert data["version"] == "slice5-activity-scene" assert data["results"]["live"] == [{"applied": True, "reason": None}] assert data["results"]["replay"] == [ {"applied": False, "reason": "duplicate"}, @@ -515,6 +606,131 @@ def test_shadow_snapshot_feeds_current_source_families_into_one_registry_owner() ] assert anchor["content"]["final_answer"] == "shadow final" +def test_activity_scene_projects_current_activity_events_for_both_render_modes(): + data = _activity_scene_snapshot() + compact = data["compact"] + transparent = data["transparent"] + + assert data["version"] == "slice5-activity-scene" + assert compact["version"] == "activity_scene_v1" + assert transparent["version"] == "activity_scene_v1" + assert compact["mode"] == "compact_worklog" + assert transparent["mode"] == "transparent_stream" + assert compact["final_answer"] == "final answer" + assert transparent["final_answer"] == "final answer" + assert compact["terminal_state"] == "completed" + assert transparent["terminal_state"] == "completed" + + compact_rows = compact["activity_rows"] + transparent_rows = transparent["activity_rows"] + assert [row["row_id"] for row in compact_rows] == [ + "run-scene:1", + "run-scene:2", + "run-scene:3", + "run-scene:4", + "run-scene:5", + "run-scene:6", + ] + assert [row["row_id"] for row in transparent_rows] == [ + row["row_id"] for row in compact_rows + ] + assert [row["kind"] for row in compact_rows] == [ + "process_prose", + "reasoning", + "tool_started", + "tool_updated", + "tool_completed", + "terminal_status", + ] + assert [row["role"] for row in compact_rows] == [ + "prose", + "thinking", + "tool", + "tool", + "tool", + "terminal", + ] + assert [row["display_hint"] for row in compact_rows] == [ + "main_prose", + "collapsed_thinking", + "tool_row", + "tool_row", + "tool_row", + "terminal_status_row", + ] + assert all(row["display_hint"] == "chronological_activity" for row in transparent_rows) + assert compact_rows[0]["text"] == "progress" + assert compact_rows[0]["tool_call_id"] is None + assert compact_rows[2]["tool_call_id"] == "tool-1" + assert compact_rows[4]["text"] == "done" + assert compact_rows[1]["thinking"] == { + "text": "private thinking", + "preview": "private thinking", + "dedupe_key": "thinking:private thinking", + } + assert compact_rows[2]["group"] == { + "group_key": "segment:3", + "activity_burst_id": 7, + "activity_segment_seq": 3, + "assistant_msg_idx": 12, + } + assert compact_rows[2]["tool"] == { + "id": "tool-1", + "name": "terminal", + "args": {"command": "rg anchor static"}, + "preview": "rg anchor static", + "snippet": "", + "result": None, + "output": None, + "done": False, + "is_error": False, + "duration": None, + "started_at": 1781200000, + "signature": 'terminal|tool-1|{"command":"rg anchor static"}', + } + assert compact_rows[4]["tool"]["done"] is True + assert compact_rows[4]["tool"]["is_error"] is False + assert compact_rows[4]["tool"]["duration"] == 1.25 + assert compact_rows[4]["tool"]["snippet"] == "done" + assert compact_rows[4]["display_hints"] == { + "compact_worklog": "tool_row", + "transparent_stream": "chronological_activity", + } + seqless_ids = [row["row_id"] for row in data["seqless"]["activity_rows"]] + assert len(seqless_ids) == len(set(seqless_ids)) + assert seqless_ids == [ + "tool-same:tool:0", + "tool-same:tool_update:1", + "tool-same:tool_complete:2", + ] + assert data["zero"]["activity_rows"][0]["group"] == { + "group_key": "segment:0", + "activity_burst_id": 0, + "activity_segment_seq": 0, + "assistant_msg_idx": 0, + } + + +def test_activity_scene_is_renderer_neutral_and_empty_safe(): + data = _activity_scene_snapshot() + compact = data["compact"] + empty = data["empty"] + + assert "final answer" not in [row["text"] for row in compact["activity_rows"]] + assert compact["identity"]["session_id"] == "sid-scene" + assert compact["identity"]["run_id"] == "run-scene" + assert compact["identity"]["stream_id"] == "stream-scene" + assert empty == { + "version": "activity_scene_v1", + "mode": "transparent_stream", + "identity": {"source_message_refs": []}, + "lifecycle": {}, + "final_answer": "", + "final_message_ref": None, + "terminal_state": None, + "activity_rows": [], + } + def test_final_projection_routes_settled_assistant_message_through_anchor_owner(): data = _final_projection_snapshot() @@ -522,7 +738,7 @@ def test_final_projection_routes_settled_assistant_message_through_anchor_owner( registry = projected["registry"] anchor = registry["anchor"] - assert data["version"] == "slice4-final-projection" + assert data["version"] == "slice5-activity-scene" assert projected["applied"] is True assert projected["reason"] is None assert projected["final_message_ref"] == "message-final" @@ -589,8 +805,7 @@ def test_registry_instances_do_not_share_owner_state(): assert isolated["stats"]["applied"] == 0 assert isolated["anchor"]["activity_events"] == [] - -def test_slice4_projection_does_not_wire_registry_into_rendering_hot_paths(): +def test_slice5_scene_projection_does_not_wire_activity_scene_into_rendering_hot_paths(): helper_names = [ "createAssistantTurnAnchorRegistry", "applyAssistantTurnAnchorNormalizedEvent", @@ -603,3 +818,6 @@ def test_slice4_projection_does_not_wire_registry_into_rendering_hot_paths(): assert helper not in _read(SESSIONS_JS) assert helper not in _read(MESSAGES_JS) assert "projectAssistantTurnAnchorSettledMessageFinalAnswer" in _read(UI_JS) + assert "projectAssistantTurnAnchorActivityScene" not in _read(UI_JS) + assert "projectAssistantTurnAnchorActivityScene" not in _read(SESSIONS_JS) + assert "projectAssistantTurnAnchorActivityScene" not in _read(MESSAGES_JS)