feat: add --branches main to drop rewound-and-abandoned forks - #300
feat: add --branches main to drop rewound-and-abandoned forks#300KKingdong wants to merge 1 commit into
Conversation
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
📝 WalkthroughWalkthroughAdds a ChangesBranch selection and rewind pruning
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
--branchesis dropped on the parallel (multi-job) conversion path.The
ProcessPoolExecutorkwargs dict forwardswrite_combined,no_timestamps, andno_recaps(all threaded alongsidebranchesin this same PR) plus every older flag, but omits"branches": branches._convert_project_workerwill therefore run withconvert_jsonl_to's defaultbranches="all"for every project processed in parallel. Sincejobsdefaults toos.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 mainsilently has no effect there, working only when--jobs 1forces the inline_convert_plan_inlinepath (Line 3680, which correctly passesbranches=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 winInclude the branch mode in the render/cache variant.
toggling --brancheschooses betweenallandmain, butvariant_suffix(...)and the generatedoutput_path/cache key only include depth/format/compact/timestamp/recap options. The same project can cache--branches alloutput and return it for a subsequent--branches mainrun without regeneration, so includebranchesin the variant and propagate it through stale/session lookups. Also, pagination currently uses cachedtotal_message_countinstead 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
branchesparam 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) takebranches: strbut never reference it again — it isn't forwarded torender_normalized_session_file(Lines 135-147) orrender_provider_wholesale(Lines 243-261). So--provider <p> ... --branches mainsilently 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 extensiveconflictslist at Lines 1147-1198, which doesn't includebranches). As written, a user combining--providerwith--branches maingets no error and no effect, with nothing indicating the flag was ignored.Consider either wiring
branchesthrough (if/when provider-side pruning is implemented) or adding it to theconflictslist 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 | 🔵 TrivialLGTM on the new
--branchesoption (choices/default/help text all consistent with the PR's described semantics).Reminder: run
just cibefore pushing these changes.Based on coding guidelines: "
**/*: Before pushing changes, remind the user to runjust 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 winPrefer
dataclasses.replaceover manual field enumeration.All 8
SessionDAGLinefields 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 fromdag_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
📒 Files selected for processing (4)
claude_code_log/cli.pyclaude_code_log/converter.pyclaude_code_log/dag.pytest/test_branch_pruning.py
| # 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) | ||
|
|
There was a problem hiding this comment.
🗄️ 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.
| # 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.
| 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 |
There was a problem hiding this comment.
🗄️ 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=pyRepository: 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_logRepository: 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')}")
PYRepository: 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}")
PYRepository: 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.pyRepository: 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}")
PYRepository: 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.
|
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), 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 |
|
One more suggestion: instead of |
Problem
Rewinding a Claude Code session —
/rewind, or editing an earlier prompt — does notdelete anything. It appends a message whose
parentUuidpoints at an earliermessage, so the transcript becomes a tree and every abandoned attempt stays in the
JSONL forever.
claude-code-logalready models this correctly:dag.pywalks the forks and renderseach 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:
uuidis_branch)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 —
isSidechainstays
Falseon every node in a rewound tree (anthropics/claude-code#24471). The shapeof the graph is the only signal available.
Solution
New CLI option, defaulting to current behaviour:
select_main_lines()keeps, per trunk, the single root→leaf path carrying the mostmessages, 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
attachmentwhose path is 12 entries; thethread 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_branchAlso 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 sessioncontinues on the branch line. Dropping
is_branchlines 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_messagesand before rendering. Once theabandoned 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
--branches all--branches mainBranchsectionsTests
test/test_branch_pruning.py, 7 cases:all) still produces every fork — behaviour unchangedis_branchlines survive re-analysis)uuid(summaries) pass throughExisting suite: no regressions. Pre-existing failures in my environment
(
test_tui.py,*_browser.py,test_index_timezone.py) fail identically onmainand 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 ispassed.
Possible follow-ups (not in this PR)
--no-excerpt-titles— message headings repeat the body's opening line, which is theother half of the duplication described above (
markdown/renderer.py,_excerpt()).--no-recapsleaves an empty#### Recapheading + timestamp behind for one of fourrecaps in my sample; the body is removed but the section is not. Happy to file
separately with a repro.
file-history-deltais reported as an unrecognized message type.Summary by CodeRabbit
--branchesoption to control transcript rendering.