feat(#6220): hydrate ID-linked historical tool transcripts into Anchor#6297
feat(#6220): hydrate ID-linked historical tool transcripts into Anchor#6297webtecnica wants to merge 1 commit into
Conversation
…to Anchor Add _hydrateHistoricalToolTranscriptAnchorScenes() which scans settled messages for eligible assistant turns with ID-linked tool_calls[] and matching role:tool result messages. Builds deterministic activity_scene_v1 scenes and attaches them as _anchor_activity_scene so the existing _renderSettledAnchorSceneForMessage path renders them — the legacy fallback in renderMessages skips anchor-owned turns. Fail closed on: - Missing/empty tool_call id - Unmatched tool result - Cross-turn-boundary results - Duplicate tool_call ids within a message - Pre-existing _anchor_activity_scene Add regression test suite (tests/test_6220_tool_transcripts_anchor.py).
|
| Filename | Overview |
|---|---|
| static/ui.js | Adds _hydrateHistoricalToolTranscriptAnchorScenes() which scans settled messages and attaches activity_scene_v1 scenes to eligible turns; idempotency guard checks the wrong message object, causing scenes to be re-created on every renderMessages call when the scene owner differs from the tool-call-bearing assistant. |
| tests/test_6220_tool_transcripts_anchor.py | New 8-test regression suite using source-level string assertions and Node-executed JavaScript snippets; covers the main guard paths well but the idempotency test only compares JSON content rather than object identity, so it passes even when the scene is silently recreated on every call. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant RM as renderMessages()
participant HY as _hydrateHistoricalToolTranscriptAnchorScenes()
participant MSG as S.messages[]
participant RS as _renderSettledAnchorSceneForMessage()
RM->>HY: call (every render)
HY->>MSG: index all role:tool results by tool_call_id
loop each assistant message with tool_calls
HY->>MSG: check messages[i]._anchor_activity_scene
note over HY,MSG: Guard checks tool-call bearer (i),<br/>not the scene owner (ownerIdx)
HY->>MSG: validate all tool_call ids are linked within turn boundary
HY->>MSG: find turn-final assistant → ownerIdx
HY->>MSG: write messages[ownerIdx]._anchor_activity_scene (frozen)
note over HY,MSG: Re-written every call when ownerIdx ≠ i
end
RM->>RS: for each assistant segment with _anchor_activity_scene → render worklog
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant RM as renderMessages()
participant HY as _hydrateHistoricalToolTranscriptAnchorScenes()
participant MSG as S.messages[]
participant RS as _renderSettledAnchorSceneForMessage()
RM->>HY: call (every render)
HY->>MSG: index all role:tool results by tool_call_id
loop each assistant message with tool_calls
HY->>MSG: check messages[i]._anchor_activity_scene
note over HY,MSG: Guard checks tool-call bearer (i),<br/>not the scene owner (ownerIdx)
HY->>MSG: validate all tool_call ids are linked within turn boundary
HY->>MSG: find turn-final assistant → ownerIdx
HY->>MSG: write messages[ownerIdx]._anchor_activity_scene (frozen)
note over HY,MSG: Re-written every call when ownerIdx ≠ i
end
RM->>RS: for each assistant segment with _anchor_activity_scene → render worklog
Comments Outside Diff (1)
-
tests/test_6220_tool_transcripts_anchor.py, line 655-698 (link)Idempotency test doesn't exercise the broken case
test_6220_hydration_idempotentdetects idempotency failure by comparingJSON.stringifyof the scene before and after the second call. Because the scene content is deterministic, the strings will be equal even when the underlying object is silently replaced by a new allocation on every call. The test therefore passes even with the broken guard. To surface the actual defect, the test needs a transcript where the scene lands on a different message than the one carryingtool_calls— the layout already present intest_6220_eligible_transcript_hydrates_anchor_scene— and must assert thatmessages[ownerIdx]._anchor_activity_sceneis the same object reference before and after the second invocation (using a sentinel identity token or a wrapper that counts writes).Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (1): Last reviewed commit: "feat(#6220): hydrate ID-linked historica..." | Re-trigger Greptile
| for(let i=0;i<messages.length;i++){ | ||
| const m=messages[i]; | ||
| if(!m||m.role!=='assistant'||m._anchor_activity_scene) continue; |
There was a problem hiding this comment.
Idempotency guard misses the common case
The check m._anchor_activity_scene only prevents re-hydration when the scene was already attached to the tool-call-bearing assistant at index i. In the normal transcript layout ([user, assistant(tool_calls), tool, assistant(answer)]), the scene is written to messages[ownerIdx] (the turn-final assistant, index 3), while messages[i] (index 1) stays scene-free. On every subsequent call to renderMessages, the guard on messages[i] passes, the function re-validates the turn, builds a new frozen scene object, and overwrites messages[ownerIdx]._anchor_activity_scene with a fresh allocation — defeating the stated "Pre-existing _anchor_activity_scene" guard. A guard added just before the assignment — if(messages[ownerIdx]._anchor_activity_scene) continue; — would close this gap.
Summary
Fixes #6220 by adding
_hydrateHistoricalToolTranscriptAnchorScenes()which scans settled messages for eligible assistant turns with ID-linkedtool_calls[]and matchingrole: toolresult messages, then builds deterministicactivity_scene_v1scenes and attaches them as_anchor_activity_scene.The existing
_renderSettledAnchorSceneForMessagepath renders the scene; the legacy fallback inrenderMessagesskips anchor-owned turns.Fail-closed guards
tool_callidtool_callids within a message_anchor_activity_sceneChanges
static/ui.js: Add_hydrateHistoricalToolTranscriptAnchorScenes()+ call site inrenderMessagestests/test_6220_tool_transcripts_anchor.py: Regression test suite (8 tests covering source-level assertions and behavioral scenarios)Verification