Packages
claude_code
0.33.1
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.0
0.34.0
0.33.1
0.32.2
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Claude Agent SDK for Elixir – Build AI agents with Claude Code
Current section
Files
Jump to
Current section
Files
lib/claude_code/history/conversation_chain.ex
# Builds conversation chains from JSONL transcript entries using `parentUuid` links.
#
# Ports the Python SDK's `_build_conversation_chain` algorithm:
# 1. Index all entries by `uuid`
# 2. Find leaf nodes (entries whose uuid appears in no other entry's parentUuid)
# 3. Among leaves, prefer main chain (not sidechain/team/meta), pick highest file position
# 4. Walk from leaf to root via `parentUuid`
# 5. Reverse for chronological order
# 6. Filter to visible messages (user/assistant, not meta/sidechain/team)
defmodule ClaudeCode.History.ConversationChain do
@moduledoc false
alias ClaudeCode.History.SessionMessage
# Transcript entry types that carry uuid + parentUuid chain links.
@transcript_entry_types ["user", "assistant", "progress", "system", "attachment"]
@doc """
Parses JSONL content into transcript entries.
Only keeps entries that have a uuid and are transcript message types.
"""
@spec parse_entries(String.t()) :: [map()]
def parse_entries(content) do
content
|> String.split("\n", trim: true)
|> Enum.flat_map(fn line ->
case Jason.decode(String.trim(line)) do
{:ok, %{"type" => type, "uuid" => uuid} = entry}
when is_binary(uuid) ->
if type in @transcript_entry_types, do: [entry], else: []
_ ->
[]
end
end)
end
@doc """
Builds the conversation chain by finding the leaf and walking `parentUuid`.
Returns entries in chronological order (root -> leaf).
`logicalParentUuid` (set on compact_boundary entries) is intentionally
NOT followed, matching VS Code IDE behavior.
"""
@spec build(list(map())) :: list(map())
def build([]), do: []
def build(entries) do
# Index by uuid for O(1) parent lookup
by_uuid = Map.new(entries, fn entry -> {entry["uuid"], entry} end)
# Build index of entry positions for tie-breaking
entry_index =
entries
|> Enum.with_index()
|> Map.new(fn {entry, idx} -> {entry["uuid"], idx} end)
# Find terminal entries (no children point to them via parentUuid)
parent_uuids =
entries
|> Enum.flat_map(fn entry ->
case entry["parentUuid"] do
parent when is_binary(parent) -> [parent]
_ -> []
end
end)
|> MapSet.new()
terminals = Enum.filter(entries, fn e -> e["uuid"] not in parent_uuids end)
# From each terminal, walk back to find the nearest user/assistant leaf
leaves = find_leaves(terminals, by_uuid)
case leaves do
[] ->
[]
_ ->
# Pick the leaf from the main chain (not sidechain/team/meta)
leaf = pick_best_leaf(leaves, entry_index)
# Walk from leaf to root via parentUuid
# walk_to_root prepends entries, so result is already root→leaf (chronological)
walk_to_root(leaf, by_uuid)
end
end
@doc """
Filters entries to only visible user/assistant messages.
"""
@spec filter_visible(list(map())) :: list(map())
def filter_visible(entries) do
Enum.filter(entries, &visible_message?/1)
end
@doc """
Converts a transcript entry to a `SessionMessage` struct with parsed content.
"""
@spec to_session_message(map()) :: SessionMessage.t()
def to_session_message(entry) do
SessionMessage.from_entry(entry)
end
# -- Private ----------------------------------------------------------------
defp find_leaves(terminals, by_uuid) do
Enum.flat_map(terminals, fn terminal ->
case walk_to_user_assistant(terminal, by_uuid, MapSet.new()) do
nil -> []
leaf -> [leaf]
end
end)
end
defp walk_to_user_assistant(nil, _by_uuid, _seen), do: nil
defp walk_to_user_assistant(entry, by_uuid, seen) do
uuid = entry["uuid"]
if uuid in seen do
nil
else
seen = MapSet.put(seen, uuid)
if entry["type"] in ["user", "assistant"] do
entry
else
parent_uuid = entry["parentUuid"]
if is_binary(parent_uuid) do
walk_to_user_assistant(Map.get(by_uuid, parent_uuid), by_uuid, seen)
end
end
end
end
defp pick_best_leaf(leaves, entry_index) do
main_leaves =
Enum.filter(leaves, fn leaf ->
not (leaf["isSidechain"] == true) and
is_nil(leaf["teamName"]) and
not (leaf["isMeta"] == true)
end)
candidates = if main_leaves == [], do: leaves, else: main_leaves
Enum.max_by(candidates, fn leaf ->
Map.get(entry_index, leaf["uuid"], -1)
end)
end
defp walk_to_root(leaf, by_uuid) do
do_walk_to_root(leaf, by_uuid, MapSet.new(), [])
end
defp do_walk_to_root(nil, _by_uuid, _seen, chain), do: chain
defp do_walk_to_root(entry, by_uuid, seen, chain) do
uuid = entry["uuid"]
if uuid in seen do
chain
else
seen = MapSet.put(seen, uuid)
chain = [entry | chain]
parent_uuid = entry["parentUuid"]
if is_binary(parent_uuid) do
do_walk_to_root(Map.get(by_uuid, parent_uuid), by_uuid, seen, chain)
else
chain
end
end
end
defp visible_message?(entry) do
entry["type"] in ["user", "assistant"] and
not (entry["isMeta"] == true) and
not (entry["isSidechain"] == true) and
is_nil(entry["teamName"])
end
end