Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions compact.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ def cmd_compact(args: argparse.Namespace) -> int:

if total_tokens <= args.budget:
console.print(f"[green]Already within budget ({total_tokens:,} <= {args.budget:,}), nothing to compact.[/green]")
if args.output:
from lib.selector import SelectionResult
# Wrap in SelectionResult and just use all turns as kept_turns
result = SelectionResult(
kept_turns=turns,
kept_scored=[],
dropped_turns=[],
budget=args.budget,
short_threshold=args.short_threshold,
user_tokens=sum(token_counts.get(t.index, 0) for t in user_turns),
short_system_tokens=sum(token_counts.get(t.index, 0) for t in short_system),
scored_kept_tokens=0,
scored_dropped_tokens=0,
total_input_tokens=total_tokens,
)
fmt = getattr(args, "format", "jsonl")
if fmt == "summary":
write_summary_text(result, args.output)
else:
write_compacted_jsonl(result, args.output)
return 0

# Identify long system turns that need scoring
Expand Down
23 changes: 23 additions & 0 deletions lib/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,29 @@ def write_summary_text(result: SelectionResult, output_path: Path) -> None:

def write_compacted_jsonl(result: SelectionResult, output_path: Path) -> None:
"""Write kept turns back to a JSONL file."""

# Fix the parentUuid chain so claude-code can load the transcript
# without breaking at missing messages.
last_uuid = None
for turn in result.kept_turns:
# Find the first message in this turn to link to the previous turn
first_msg_idx = -1
for i, record in enumerate(turn.lines):
if isinstance(record, dict) and record.get("type") in ("user", "assistant", "system", "attachment"):
first_msg_idx = i
break

if first_msg_idx >= 0 and last_uuid is not None:
# We ONLY rewrite parentUuid if it already exists, to avoid touching root nodes
if "parentUuid" in turn.lines[first_msg_idx]:
turn.lines[first_msg_idx]["parentUuid"] = last_uuid

# Find the last message in this turn to be the parent for the next turn
for record in reversed(turn.lines):
if isinstance(record, dict) and "uuid" in record and record.get("type") in ("user", "assistant", "system", "attachment"):
last_uuid = record["uuid"]
break

with open(output_path, "w") as f:
for turn in result.kept_turns:
for record in turn.lines:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ if uv run python compact.py "${JSONL_FILE}" \
if command -v unleash-refresh &>/dev/null; then
echo "$(date -Iseconds) Restarting via unleash-refresh" >> "${LOG_DIR}/hook.log"
unleash-refresh "COMPACT COMPLETE. Previous context has been summarized. Continue with your current task."
# If unleash-refresh returns (shouldn't normally), exit cleanly

# Block to ensure Claude Code processes the SIGINT and shuts down BEFORE this
# script exits. This prevents a race condition where the hook completes and
# Claude starts the API compaction call before the SIGINT is fully handled.
sleep 10
exit 0
else
echo "$(date -Iseconds) WARNING: unleash-refresh not found — compacted JSONL is on disk but Claude's API compact will still run over it" >> "${LOG_DIR}/hook.log"
Expand Down