diff --git a/compact.py b/compact.py index 5bf1500..f45b83f 100644 --- a/compact.py +++ b/compact.py @@ -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 diff --git a/lib/formatter.py b/lib/formatter.py index a824d71..4b42f11 100644 --- a/lib/formatter.py +++ b/lib/formatter.py @@ -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: diff --git a/plugins/claude-code/hooks-handlers/supercompact-precompact.sh b/plugins/claude-code/hooks-handlers/supercompact-precompact.sh index 99890f6..0e70ec3 100755 --- a/plugins/claude-code/hooks-handlers/supercompact-precompact.sh +++ b/plugins/claude-code/hooks-handlers/supercompact-precompact.sh @@ -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"