Skip to content

feat: add --branches main to drop rewound-and-abandoned forks - #300

Open
KKingdong wants to merge 1 commit into
daaain:mainfrom
KKingdong:feat/branches-main
Open

feat: add --branches main to drop rewound-and-abandoned forks#300
KKingdong wants to merge 1 commit into
daaain:mainfrom
KKingdong:feat/branches-main

Conversation

@KKingdong

@KKingdong KKingdong commented Jul 26, 2026

Copy link
Copy Markdown

Problem

Rewinding a Claude Code session — /rewind, or editing an earlier prompt — does not
delete anything. It appends a message whose parentUuid points at an earlier
message, so the transcript becomes a tree and every abandoned attempt stays in the
JSONL forever.

claude-code-log already models this correctly: dag.py walks the forks and renders
each one as its own Branch • <uuid8> • <preview> section. That is the right default —
nothing is hidden from the reader.

For reading back a long working session, though, it is noise. Measured on a real
4 MB / 1451-line transcript of mine:

count
entries with a uuid 1030
leaves (conversation tips) 31
DAG-lines 12 (11 of them is_branch)
abandoned entries 55 (3% of the file)
abandoned user prompts 11 of 43 (26%)

The byte cost is small, but one in four questions the reader scrolls past was rewound
away and never answered. Because each fork also gets a section header and every
message title repeats the message's own opening line as an excerpt, the same question
can appear three times before its answer.

There is no flag to filter on: Anthropic never marks dead branches — isSidechain
stays False on every node in a rewound tree (anthropics/claude-code#24471). The shape
of the graph is the only signal available.

Solution

New CLI option, defaulting to current behaviour:

--branches [all|main]   [default: all]

select_main_lines() keeps, per trunk, the single root→leaf path carrying the most
messages, and truncates each ancestor at the fork point it was left at.

Why longest path, not latest timestamp

The obvious rule — "keep the newest leaf" — is wrong on real data. In the sample
transcript the newest leaf is a stray attachment whose path is 12 entries; the
thread the session actually spent its time on is 975. Selecting by recency keeps
the stub and discards the work. Newest leaves are routinely a one-line afterthought
typed after a rewind.

Why not just filter is_branch

Also wrong, and worth stating because it looks correct. In the sample, the last real
exchange lives on Branch • d07feb77, not on the trunk: after a rewind the session
continues on the branch line. Dropping is_branch lines discards the most recent work.
The surviving path legitimately traverses 5 branch lines.

Sidechains

Never compete. Subagent transcripts are real work branching off the trunk, not
abandoned attempts, so they are excluded from the selection and re-admitted whenever
their anchor message survives the prune.

Where the filter runs

At the entry level, after deduplicate_messages and before rendering. Once the
abandoned entries are gone there is nothing left to fork, so the DAG the renderer
builds is already linear and every existing branch code path naturally emits nothing.
No renderer changes were needed. A prebuilt SessionTree (directory mode,
workflow runs) is rebuilt from the survivors with its workflow links carried across.

Provider-wholesale rendering (Codex et al.) is deliberately untouched — the
rewind-fork shape is specific to Claude Code's session format.

Result

claude-code-log s.jsonl --format md --detail minimal --compact --no-recaps --branches main
--branches all --branches main
size 105 KB 101 KB
lines 1731 1656
Branch sections 11 0
user prompts 24 18

Tests

test/test_branch_pruning.py, 7 cases:

  • abandoned branch and its reply are dropped
  • longest path wins over a later stray prompt
  • default (all) still produces every fork — behaviour unchanged
  • pruned result is linear (no is_branch lines survive re-analysis)
  • ancestor is truncated at the fork point, keeping the fork message itself
  • a transcript with no rewinds passes through identically
  • entries without a uuid (summaries) pass through

Existing suite: no regressions. Pre-existing failures in my environment
(test_tui.py, *_browser.py, test_index_timezone.py) fail identically on main
and are missing-optional-dependency errors, not related to this change.

Backward compatibility

Default is --branches all. Output is byte-identical to before unless the flag is
passed.

Possible follow-ups (not in this PR)

  • --no-excerpt-titles — message headings repeat the body's opening line, which is the
    other half of the duplication described above (markdown/renderer.py, _excerpt()).
  • --no-recaps leaves an empty #### Recap heading + timestamp behind for one of four
    recaps in my sample; the body is removed but the section is not. Happy to file
    separately with a repro.
  • file-history-delta is reported as an unrecognized message type.

Summary by CodeRabbit

  • New Features
    • Added a --branches option to control transcript rendering.
    • Choose between displaying all conversation branches or only the main path.
    • Main-path rendering removes abandoned rewind branches while preserving relevant conversation history.
  • Bug Fixes
    • Improved branch selection to retain the longest surviving conversation path rather than relying on timestamps.
    • Preserved summaries and other transcript entries without identifiers during branch filtering.

Rewinding a Claude Code session (/rewind, or editing an earlier prompt)
does not delete anything. It appends a message whose parentUuid points at
an earlier message, so the transcript becomes a tree and every abandoned
attempt stays in the JSONL. claude-code-log already models this correctly
and renders each fork as its own "Branch • <uuid8> • <preview>" section,
which is the right default: nothing is hidden from the reader.

For reading back a long working session, though, it is noise. On a real
4 MB, 1451-line transcript the abandoned forks are only 3% of the bytes
but 11 of 43 user prompts — one in four questions the reader scrolls past
was rewound away and never answered. Combined with the repeated section
headers, the same question can appear three times.

Anthropic does not mark dead branches: isSidechain stays False on every
node in a rewound tree (anthropics/claude-code#24471), so there is no flag
to filter on. The shape of the graph is the only signal available.

select_main_lines() keeps, per trunk, the single root->leaf path carrying
the most messages, and truncates each ancestor at the fork point it was
left at. Sidechains never compete: subagent transcripts are real work
branching off the trunk, and are kept whenever their anchor survives.

Longest path, not latest timestamp. In the sample transcript the newest
leaf is a stray attachment whose path is 12 entries; the thread the
session actually spent its time on is 975. Selecting by recency keeps the
stub and discards the work.

The filter runs at the entry level, after dedup and before rendering, so
the DAG the renderer builds is already linear and every existing branch
code path naturally emits nothing. No renderer changes were needed.

Default is --branches all, so existing output is byte-identical.
Provider-wholesale rendering (Codex et al.) is untouched: the rewind-fork
shape is specific to Claude Code's session format.

Verified on the sample transcript with
  --detail minimal --compact --no-recaps --branches main
  branch sections 11 -> 0, user prompts 24 -> 18, 105 KB -> 101 KB
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a --branches [all|main] CLI option. Main-line mode prunes abandoned rewind branches through DAG selection and applies consistently across provider, session, hierarchy, auto-detected, streaming, and combined conversion paths.

Changes

Branch selection and rewind pruning

Layer / File(s) Summary
DAG main-line pruning
claude_code_log/dag.py, test/test_branch_pruning.py
DAG helpers select longest surviving trunk paths, retain valid sidechains, filter UUID-bearing entries, and test pruning, truncation, linearity, unchanged sessions, and UUID-less entries.
Conversion pipeline integration
claude_code_log/converter.py
Conversion entry points accept branches, filter messages in main mode, rebuild pruned session DAGs, and preserve workflow mappings.
CLI option and routing
claude_code_log/cli.py
The CLI defines `--branches [all

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant convert_jsonl_to
  participant filter_to_main_line
  participant select_main_lines
  CLI->>convert_jsonl_to: branches=main
  convert_jsonl_to->>filter_to_main_line: deduplicated messages
  filter_to_main_line->>select_main_lines: session DAG lines
  select_main_lines-->>filter_to_main_line: retained main-line UUIDs
  filter_to_main_line-->>convert_jsonl_to: filtered messages
Loading

Possibly related PRs

  • daaain/claude-code-log#272: Both changes thread additional conversion options through process_projects_hierarchy and CLI routing.

Suggested reviewers: cboos

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding a main-branch option to prune abandoned rewound forks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
claude_code_log/converter.py (2)

3703-3729: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

--branches is dropped on the parallel (multi-job) conversion path.

The ProcessPoolExecutor kwargs dict forwards write_combined, no_timestamps, and no_recaps (all threaded alongside branches in this same PR) plus every older flag, but omits "branches": branches. _convert_project_worker will therefore run with convert_jsonl_to's default branches="all" for every project processed in parallel. Since jobs defaults to os.cpu_count() (Line 3653) whenever the user doesn't pass --jobs 1, this is the default execution path for --all-projects/no-INPUT_PATH runs — --branches main silently has no effect there, working only when --jobs 1 forces the inline _convert_plan_inline path (Line 3680, which correctly passes branches=branches).

🐛 Proposed fix
                             "write_combined": write_combined,
                             "no_timestamps": no_timestamps,
                             "no_recaps": no_recaps,
+                            "branches": branches,
                         },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/converter.py` around lines 3703 - 3729, Update the kwargs
dictionary submitted to _convert_project_worker in the ProcessPoolExecutor path
to include the existing branches value, alongside write_combined, no_timestamps,
and no_recaps. Preserve the inline _convert_plan_inline behavior and ensure
parallel conversions pass the user-selected branches instead of relying on
convert_jsonl_to’s default.

1946-1949: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include the branch mode in the render/cache variant.

toggling --branches chooses between all and main, but variant_suffix(...) and the generated output_path/cache key only include depth/format/compact/timestamp/recap options. The same project can cache --branches all output and return it for a subsequent --branches main run without regeneration, so include branches in the variant and propagate it through stale/session lookups. Also, pagination currently uses cached total_message_count instead of the post-prune message count, so it can pick the wrong page size for the requested branch mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/converter.py` around lines 1946 - 1949, Update variant_suffix
and all generated output/cache identifiers to include the selected branches mode
(all or main), and propagate it through stale and session lookups so
branch-specific results cannot be reused. In pagination, use the post-prune
message count for the requested branch mode instead of cached
total_message_count when determining page size.
claude_code_log/cli.py (1)

94-106: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

branches param is accepted but never used in provider rendering paths.

Both _render_provider_input_file (added param at Line 104) and _run_provider_wholesale (added param at Line 208) take branches: str but never reference it again — it isn't forwarded to render_normalized_session_file (Lines 135-147) or render_provider_wholesale (Lines 243-261). So --provider <p> ... --branches main silently behaves exactly like --branches all. The PR description notes "Provider-wholesale rendering remains unchanged," which may make this intentional for now, but it breaks the file's own stated convention that unsupported flag combinations for provider mode are rejected LOUDLY rather than silently ignored (Lines 1116-1117, and the extensive conflicts list at Lines 1147-1198, which doesn't include branches). As written, a user combining --provider with --branches main gets no error and no effect, with nothing indicating the flag was ignored.

Consider either wiring branches through (if/when provider-side pruning is implemented) or adding it to the conflicts list for provider mode so the CLI fails loudly instead of silently no-op'ing, consistent with the rest of this function's design.

Also applies to: 198-219

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/cli.py` around lines 94 - 106, Handle the unused branches
parameter in _render_provider_input_file and _run_provider_wholesale: either
forward it through the provider rendering calls when branch pruning is
supported, or add branches to the provider-mode conflicts validation so
combining --provider with --branches fails explicitly. Do not leave the option
silently ignored.
🧹 Nitpick comments (2)
claude_code_log/cli.py (1)

1038-1051: 📐 Maintainability & Code Quality | 🔵 Trivial

LGTM on the new --branches option (choices/default/help text all consistent with the PR's described semantics).

Reminder: run just ci before pushing these changes.

Based on coding guidelines: "**/*: Before pushing changes, remind the user to run just ci."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/cli.py` around lines 1038 - 1051, Before pushing the changes,
run the full CI suite with just ci and address any failures.

Source: Coding guidelines

claude_code_log/dag.py (1)

1260-1269: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer dataclasses.replace over manual field enumeration.

All 8 SessionDAGLine fields are correctly listed today, but this manual reconstruction silently drops any field added to the dataclass in the future (a new field would default rather than copy from dag_line). dataclasses.replace(dag_line, uuids=dag_line.uuids[:cut]) removes that maintenance hazard.

♻️ Proposed refactor
-            cut = dag_line.uuids.index(fork_uuid) + 1
-            kept[line_id] = SessionDAGLine(
-                session_id=dag_line.session_id,
-                uuids=dag_line.uuids[:cut],
-                first_timestamp=dag_line.first_timestamp,
-                parent_session_id=dag_line.parent_session_id,
-                attachment_uuid=dag_line.attachment_uuid,
-                is_branch=dag_line.is_branch,
-                original_session_id=dag_line.original_session_id,
-                is_sidechain=dag_line.is_sidechain,
-            )
+            cut = dag_line.uuids.index(fork_uuid) + 1
+            kept[line_id] = dataclasses.replace(dag_line, uuids=dag_line.uuids[:cut])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/dag.py` around lines 1260 - 1269, Replace the manual
SessionDAGLine reconstruction in the kept assignment with dataclasses.replace,
preserving dag_line and overriding only uuids with dag_line.uuids[:cut]. Ensure
the dataclasses module or replace symbol is imported as needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@claude_code_log/converter.py`:
- Around line 2671-2675: Update the per-session conversion flow around
filter_to_main_line and deduplicate_messages so messages are deduplicated before
branch pruning, matching convert_jsonl_to’s ordering. Apply deduplicate_messages
to the full messages list before filter_to_main_line when branches == "main",
then remove the redundant later deduplication of session_messages unless it is
intentionally retained as a no-op safety check.

In `@claude_code_log/dag.py`:
- Around line 1298-1313: Update filter_to_main_line to keep
SummaryTranscriptEntry leafUuid references consistent with the surviving
entries: after pruning, rewatch or filter summaries against the retained entry
set, and rebuild or clear leafUuid when its target UUID was removed. Ensure
summaries retained in the result have valid session mapping and do not leave
dangling _leaf_uuid references.

---

Outside diff comments:
In `@claude_code_log/cli.py`:
- Around line 94-106: Handle the unused branches parameter in
_render_provider_input_file and _run_provider_wholesale: either forward it
through the provider rendering calls when branch pruning is supported, or add
branches to the provider-mode conflicts validation so combining --provider with
--branches fails explicitly. Do not leave the option silently ignored.

In `@claude_code_log/converter.py`:
- Around line 3703-3729: Update the kwargs dictionary submitted to
_convert_project_worker in the ProcessPoolExecutor path to include the existing
branches value, alongside write_combined, no_timestamps, and no_recaps. Preserve
the inline _convert_plan_inline behavior and ensure parallel conversions pass
the user-selected branches instead of relying on convert_jsonl_to’s default.
- Around line 1946-1949: Update variant_suffix and all generated output/cache
identifiers to include the selected branches mode (all or main), and propagate
it through stale and session lookups so branch-specific results cannot be
reused. In pagination, use the post-prune message count for the requested branch
mode instead of cached total_message_count when determining page size.

---

Nitpick comments:
In `@claude_code_log/cli.py`:
- Around line 1038-1051: Before pushing the changes, run the full CI suite with
just ci and address any failures.

In `@claude_code_log/dag.py`:
- Around line 1260-1269: Replace the manual SessionDAGLine reconstruction in the
kept assignment with dataclasses.replace, preserving dag_line and overriding
only uuids with dag_line.uuids[:cut]. Ensure the dataclasses module or replace
symbol is imported as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8fb0ec38-8719-416d-9e0c-544c04dae17f

📥 Commits

Reviewing files that changed from the base of the PR and between 10b288d and 132f8bb.

📒 Files selected for processing (4)
  • claude_code_log/cli.py
  • claude_code_log/converter.py
  • claude_code_log/dag.py
  • test/test_branch_pruning.py

Comment on lines +2671 to +2675
# Mirror ``convert_jsonl_to``: drop rewound-and-abandoned fork branches
# so a per-session file matches the combined transcript (--branches main).
if branches == "main":
messages = filter_to_main_line(messages)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Branch pruning runs before dedup here, unlike convert_jsonl_to.

The comment claims this mirrors convert_jsonl_to, but convert_jsonl_to explicitly dedups the full message list before calling filter_to_main_line ("Runs after dedup so the DAG sees the same entries the renderer will", Line 2057). Here, filter_to_main_line(messages) runs on the raw, un-deduplicated multi-session list; deduplicate_messages only runs later (Line 2715) on the already-narrowed session_messages subset. deduplicate_messages's own docstring documents real duplicate shapes (version-stutter, same-timestamp "branch switch artifacts") that the DAG builder could misread as competing fork branches before dedup collapses them — so a session containing such duplicates could get pruned differently here than in convert_jsonl_to, defeating the stated "matches the combined transcript" goal.

🐛 Proposed fix — dedup before pruning, mirroring convert_jsonl_to
     # Load messages from JSONL files
     messages, _session_tree = load_directory_transcripts(input_path, cache_manager)
 
+    messages = deduplicate_messages(messages)
+
     # Mirror ``convert_jsonl_to``: drop rewound-and-abandoned fork branches
     # so a per-session file matches the combined transcript (--branches main).
     if branches == "main":
         messages = filter_to_main_line(messages)

(and drop the now-redundant deduplicate_messages(session_messages) call below, or leave it as a cheap no-op safety net.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Mirror ``convert_jsonl_to``: drop rewound-and-abandoned fork branches
# so a per-session file matches the combined transcript (--branches main).
if branches == "main":
messages = filter_to_main_line(messages)
# Load messages from JSONL files
messages, _session_tree = load_directory_transcripts(input_path, cache_manager)
messages = deduplicate_messages(messages)
# Mirror ``convert_jsonl_to``: drop rewound-and-abandoned fork branches
# so a per-session file matches the combined transcript (--branches main).
if branches == "main":
messages = filter_to_main_line(messages)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/converter.py` around lines 2671 - 2675, Update the
per-session conversion flow around filter_to_main_line and deduplicate_messages
so messages are deduplicated before branch pruning, matching convert_jsonl_to’s
ordering. Apply deduplicate_messages to the full messages list before
filter_to_main_line when branches == "main", then remove the redundant later
deduplication of session_messages unless it is intentionally retained as a no-op
safety check.

Comment thread claude_code_log/dag.py
Comment on lines +1298 to +1313
def filter_to_main_line(
entries: list[TranscriptEntry],
sidechain_uuids: set[str] | None = None,
) -> list[TranscriptEntry]:
"""Return ``entries`` with abandoned rewind branches removed.

Entries without a ``uuid`` (summaries, ai-titles, queue operations) do
not participate in the DAG and are passed through untouched.
"""
keep = main_line_uuids(entries, sidechain_uuids=sidechain_uuids)
result: list[TranscriptEntry] = []
for entry in entries:
uuid = getattr(entry, "uuid", None)
if uuid is None or uuid in keep:
result.append(entry)
return result

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where SummaryTranscriptEntry.leafUuid is consumed downstream.
rg -n -C3 '\.leafUuid\b' claude_code_log --type=py

Repository: daaain/claude-code-log

Length of output: 2423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dag.py relevant sections =="
sed -n '1200,1330p' claude_code_log/dag.py
echo
echo "== converter.py relevant sections =="
sed -n '980,1125p' claude_code_log/converter.py
echo
echo "== renderer.py summary mapping =="
sed -n '1110,1165p' claude_code_log/renderer.py
echo
echo "== cache.py summary handling =="
sed -n '560,605p' claude_code_log/cache.py
echo
echo "== root-level classes =="
rg -n --type=py 'class SummaryTranscriptEntry|class TranscriptEntry|def deduplicate_messages|def filter_to_main_line|def main_line_uuids' claude_code_log

Repository: daaain/claude-code-log

Length of output: 16532


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== filter_to_main_line call sites =="
rg -n 'filter_to_main_line\(' claude_code_log -g '*.py'

echo
echo "== converter.py summary construction =="
rg -n -C4 'SummaryTranscriptEntry\(|leafUuid|summary' claude_code_log/converter.py

echo
echo "== cache usage of _leaf_uuid and cache entry order/build =="
rg -n -C3 '_leaf_uuid|_deserialize_entry|serialize_entry|prepare_session_summaries|deduplicate_messages|filter_to_main_line' claude_code_log -g '*.py'

echo
echo "== deterministic probe outline =="
python3 - <<'PY'
from pathlib import Path
for p in Path("claude_code_log").glob("*"):
    if p.suffix not in {".py",".pyi"}: continue
    text=p.read_text(errors="ignore")
    print(f"{p}: leafUuid occurrences={text.count('leafUuid')}, summaries occurrences={text.count('summary')}")
PY

Repository: daaain/claude-code-log

Length of output: 26366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== converter.py around summary generation =="
sed -n '2008,2100p' claude_code_log/converter.py
echo
echo "== converter.py around per-session branch==main summaries =="
sed -n '2378,2430p' claude_code_log/converter.py
echo
echo "== renderer.py around summary collection and first text =="
sed -n '730,790p' claude_code_log/renderer.py
sed -n '1112,1150p' claude_code_log/renderer.py

echo
echo "== compute_session_data flow summary =="
python3 - <<'PY'
from pathlib import Path
p=Path("claude_code_log/converter.py")
s=p.read_text()
for name in ["def get_summary_messages","def _collect_project_sessions","def compute_session_data","def _collect_project_sessions"]:
    idx=s.find(f"def {name}")
    print(f"{name}: {idx}")
PY

Repository: daaain/claude-code-log

Length of output: 11043


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== build_session_data / summary messages area =="
sed -n '2028,2065p' claude_code_log/converter.py
echo
echo "== renderer.py summary messages first_user text collection area =="
sed -n '1025,1095p' claude_code_log/renderer.py
sed -n '1096,1112p' claude_code_log/renderer.py

echo
echo "== summary output summary messages area in model serialization/collector =="
rg -n -C4 'summary_messages|SessionSummary|session_summaries\(|first_user_message|title or first_user' claude_code_log/cache.py claude_code_log/renderer.py claude_code_log/converter.py -g '*.py'

echo
echo "== exact call order in renderer.py =="
sed -n '740,770p'claude_code_log/renderer.py

Repository: daaain/claude-code-log

Length of output: 32050


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== renderer.py summary messages construction =="
sed -n '1035,1090p' claude_code_log/renderer.py
echo
echo "== models.py SummaryTranscriptEntry =="
sed -n '265,285p' claude_code_log/models.py

echo
echo "== summary text consumers and first_user fallback only area =="
python3 - <<'PY'
from pathlib import Path
import re
for p in Path("claude_code_log").glob("*.py"):
    s=p.read_text(errors="ignore")
    hits=[]
    for n,line in enumerate(s.splitlines(),1):
        if "first_user_message" in line or "prepare_session_summaries" in line:
            hits.append((n,line.rstrip()))
    if hits:
        print(f"\n--- {p} ---")
        for n,line in hits[:80]:
            print(f"{n}: {line}")
PY

Repository: daaain/claude-code-log

Length of output: 6417


Update filter_to_main_line to keep summary sessions coherent.

After --branches main pruning, prepare_session_summaries() still includes the original SummaryTranscriptEntry, but its leafUuid may now point at a removed message. The current map then gets no sessionId for that target UUID, so the summary can disappear from session_nav/JSON while remaining in the cache as a dangling _leaf_uuid. Rewatch/filter summaries after pruning, or rebuild/clear leafUuid when the target message is not in the surviving entry set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/dag.py` around lines 1298 - 1313, Update filter_to_main_line
to keep SummaryTranscriptEntry leafUuid references consistent with the surviving
entries: after pruning, rewatch or filter summaries against the retained entry
set, and rebuild or clear leafUuid when its target UUID was removed. Ensure
summaries retained in the result have valid session mapping and do not leave
dangling _leaf_uuid references.

@cboos

cboos commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Hello! Thanks for your contribution. This looks useful indeed, though I have some qualms about selecting the "main" branch. What if the "longest" is not what you want to isolate? Say you go down a long dead end, come back, start a different, shorter, and more successful route? One possibility would be to give a short message uuid (the [uuid] button can show them), --branches uuid (or --branches uuid, uuid, ..., as you already have a plural here).

The other thing is that we take the CodeRabbit suggestions seriously, so you should address them or refute them if you find them undeserved (but here they seem justified).

@KKingdong

Copy link
Copy Markdown
Author

Hello! Thanks for your contribution. This looks useful indeed, though I have some qualms about selecting the "main" branch. What if the "longest" is not what you want to isolate? Say you go down a long dead end, come back, start a different, shorter, and more successful route? One possibility would be to give a short message uuid (the [uuid] button can show them), --branches uuid (or --branches uuid, uuid, ..., as you already have a plural here).

The other thing is that we take the CodeRabbit suggestions seriously, so you should address them or refute them if you find them undeserved (but here they seem justified).

I agree with your opinion

@cboos

cboos commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

One more suggestion: instead of --branches (plural), have claude-code-log behave as of today when no --branch option is given, behave as with your patch (select "longest") when --branch main is given, and filter in all the branches containing at least one of uuid1, uuid2, ... when given --branch uuid1 --branch uuid2 ... .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants