RFD D66: Interactive Compaction Review
- Status: Draft
- Category: Design
- Authors: rgrant rgrant@contract.design
- Date: 2026-08-02
- Extends: RFD 064
Summary
This RFD adds an interactive mode to jp conversation compact. The user reviews and revises a proposed compaction by talking to the model in a separate compaction conversation, then applies the result to the source conversation. Every field of the compaction is negotiable: the turn range, the summary text, and the mechanical policies.
Motivation
RFD 064 generates a compaction summary in one non-interactive model call and applies it immediately. But:
some conversation compactions are intricate and need review; and
conversations that can't fit in context cannot be compacted except by operating over subranges, which means summarizing from prior compactions, and that needs higher-level tooling to do well.
Interactive review of compaction summaries in a flexible conversation model allows both subagent use in compaction as well as human review.
Doing nothing leaves summarization as a one-shot bet that cannot operate over long converations.
Design
Behavior
Three commands.
# 1. Start a review.
jp conversation compact --interactive
# 2. Discuss. Repeat as needed.
jp q
# 3. Apply the agreed compaction to the source conversation.
jp conversation compact --continue
# Or: stop without applying anything.
jp conversation compact --abortThe shape follows git rebase: start, then discuss, then --continue or --abort.
Step 1 forks the source conversation into a compaction conversation, makes it active, runs the existing summarizer once so the review opens on a real draft rather than a blank page, records that draft as the opening proposal, and prints how to proceed. The opening proposal is always a summary proposal. Its range comes from the range flags of compact (--keep-first, --keep-last, --from, --to) when given; otherwise from the first configured rule with a summary section; otherwise from the built-in default range. --reasoning, --tools, --dry-run, and --reset conflict with --interactive; the policies stay negotiable inside the review. Starting a second review for a source that already has one open is refused (see One review per source).
The fork copies the whole source conversation, including the most recent turns. The range is under negotiation, so the model has to see the turns that a range change would move across. Whether the source's earlier compactions come along depends on size (see Raw events and prior compactions).
Step 2 is ordinary chat. No new interaction mode, no prompt surface, no long-lived process. The user can leave and come back; the compaction conversation is durable.
Step 3 reads the most recent proposal from the compaction conversation, prints it, and asks for confirmation. On yes, it applies the proposal to the source conversation as a Compaction event, archives the compaction conversation (RFD 071), and switches the active conversation back to the source. On no, nothing changes and the review stays open.
--abort ends the review without applying anything. It archives the compaction conversation and switches back to the source.
--continue and --abort take no conversation ID and no range flags. They operate on the active conversation, which must be a compaction conversation; anything else fails with a clear error, so a user who switched away switches back first with jp conversation use.
Both endings archive rather than delete, so the discussion that produced (or failed to produce) a compaction stays available. The user can delete a compaction conversation outright if they want it gone.
The compaction conversation
The fork is not a plain copy. Step 1 sets the new conversation up for its one job:
Turn markers. Each copied turn gets a marker carrying its source turn number, prepended to the turn's user message:
[turn 17]. The markers are ordinary event content, so the model sees them in its context and the user sees them injp conversation print. Both sides read the same numbers instead of counting messages. Marker numbers are 1-based, like every user-facing turn position (Indexing and Counting Conventions). Turns added during the review carry no markers; they are not part of the negotiation.Tools. A configuration entry written at fork time disables every inherited tool and enables exactly one:
propose_compaction. The source's content is already fully in context, so the review needs no file access, and a model with edit access during what the user treats as a read-only discussion is a surprise nobody wants. Dropping the inherited tool descriptions also stops them being resent on every review turn.Instructions. The same configuration entry sets the review instructions: the model's job is to discuss the compaction and record each agreed revision by calling
propose_compaction.Model. The same configuration entry sets the review model (see Configuration).
Source ID. The compaction conversation records the source conversation's ID in a new field on the conversation record — a sibling of
pinned_atandarchived_at, not event metadata. Its presence is what marks a conversation as a compaction conversation, and it survives archiving, so an archived review still names what it reviewed.
One review per source
--interactive refuses to start when a not-yet-archived compaction conversation already names the source, and points at it:
error: a review of this conversation is already open: 01xyz
switch to it with `jp conversation use 01xyz`, then discuss, `--continue`, or `--abort`Two open reviews would each carry a full copy of the source, and whichever finalized second would append a second compaction on top of the first, with RFD 064's overlap rules quietly deciding the outcome. There is no use case for that. Archived reviews never block: both endings archive, so a finished review clears the way, and a forgotten one is exactly what the error reminds the user about. Unarchiving an old review reopens it: new reviews and compaction of the source are blocked again, and --continue offers its stale proposal. Re-validation and the confirmation prompt make that safe, if rarely useful.
Compaction of the source is blocked
An open review freezes the source's compaction state, the way git blocks conflicting operations mid-rebase. While a not-yet-archived compaction conversation names the source, JP refuses every operation that appends or removes Compaction events on the source's stream: jp conversation compact in all forms, including --reset, and jp query --compact targeting the source.
error: a compaction review of this conversation is open: 01xyz
finish it first: `jp conversation use 01xyz`, then discuss, `--continue`, or `--abort`Without the block, a --reset mid-review changes what the copied compaction events in the fork mean, and a new summary compaction triggers RFD 064's overlap auto-extension at apply time, silently widening the range the review negotiated.
The block is scoped to compaction writes. --dry-run stays available; it writes nothing. Ordinary chat in the source stays available; the proposal's absolute range already tolerates growth (see Range bounds are absolute). jp conversation fork --compact stays available; it compacts the fork's copy and leaves the source's stream untouched. The check is the same open-review scan --interactive performs.
What is negotiated
The unit under discussion is a compaction proposal: one candidate Compaction for the source conversation. Its fields are those RFD 064 already defines.
| Field | Negotiable | Example change |
|---|---|---|
from_turn | yes | "start at 4, turns 1 to 3 set up the whole task" |
to_turn | yes | "stop at 17, turn 18 opens a new topic" |
summary | yes | "keep the retry decision, drop the file listing" |
reasoning | yes | "do not strip reasoning, it carries the rationale" |
tool_calls | yes | "strip responses but keep the arguments" |
Turn numbers refer to the source conversation, read from the turn markers. The compaction conversation is scratch space, not the subject.
The fields follow RFD 064's precedence: when summary is set, reasoning and tool_calls are ignored. Validation flags a proposal that sets summary alongside either, so the model drops the dead fields rather than letting the user negotiate policies that change nothing.
Recording a proposal
The model records a proposal by calling a built-in tool.
propose_compaction(from_turn, to_turn, summary, reasoning, tool_calls)Built-in means implemented inside JP rather than as an external program. The tool is disabled by default; the configuration entry written at fork time enables it inside compaction conversations, and nowhere else. Outside a review, the model never sees it.
from_turn and to_turn are 1-based source turn positions — the numbers the model reads from the turn markers. The model never sees a 0-based index: markers, tool arguments, validation errors, and the rendered proposal all speak 1-based. JP translates to the 0-based Compaction fields only when --continue builds the event, at the same boundary where CLI range flags are translated (Indexing and Counting Conventions).
Each call is one proposal. Calls accumulate as ordinary tool-call events in the compaction conversation, so the revision history is the conversation history. --continue reads the most recent call.
Three properties follow from using a tool call rather than message text:
- Chat after a proposal does not invalidate it. The proposal is a distinct event, not a position in the message list.
- JP already renders tool calls, so each revision is visible on screen as it is made.
- The range and the summary are recorded by the same action, so they cannot describe different things.
JP validates every call as it is made, against the source conversation as it stands: the range falls within the source's turns, from_turn does not exceed to_turn, and the policy values parse. Validation also applies RFD 064's summary-overlap rule at once: a summary range that partially overlaps an existing summary compaction is grown to subsume it, and the grown range is the proposal's range from that moment on. The tool result, the confirmation prompt, and the stored event all carry the same numbers; nothing widens at apply time. An invalid call returns an error as the tool result ("the conversation has 20 turns, to_turn was 25"), which the model reads and corrects in the same exchange, while the user watches. A valid call returns the proposal rendered for display, so the user and the model see the same confirmed reading of what was just agreed. A call made in a conversation that records no source is an error.
Validation reads the source without taking its lock. A snapshot suffices: an open review admits no compaction writes, new turns only widen the valid range, and --continue re-validates under the source's lock before applying.
--continue fails with a clear error when the compaction conversation contains no propose_compaction call, and re-validates the call it finds before applying it.
The opening proposal
Step 1 records the summarizer's draft as the first proposal by writing the tool-call events itself: an assistant propose_compaction call whose arguments are the summarizer's output and the seeded range, plus the matching result. The model never made this call; it finds it in its history and treats it as its own, which models handle fine in practice. Writing the events directly keeps the opening state deterministic and the recorded arguments byte-for-byte the summarizer's output. Asking the model to record the draft itself would cost one more model call and lose the guarantee: the model can reword the summary while copying it, or fail to call the tool at all.
Range bounds are absolute
A range agreed in conversation is usually expressed relative to the end of the conversation: "keep the last three turns". propose_compaction records it as absolute source-conversation turn numbers, resolved against the conversation as it stood when the proposal was made. "Keep the last three of twenty" is recorded as turns 1 to 17, and displayed that way:
Proposed: compact turns 1-17 of 20 (summarize)This matches RFD 064, which already resolves all range bounds to absolute turn indices at creation time.
The consequence is that a proposal does not move. If the source conversation grows to twenty-two turns while the review is open, --continue still compacts turns 1 to 17. Turns 18 to 22 stay uncompacted and remain available to a later compaction. A relative bound would silently widen the range to cover turns the review never looked at.
Applying and aborting
--continue resolves the source from the active compaction conversation's source field, so the user passes no IDs. It reads the compaction conversation and writes to the source, so it locks both.
Before touching anything, it prints the proposal it found and asks:
Proposed: compact turns 1-17 (summarize)
The conversation now has 24 turns; 7 turns stay uncompacted.
<summary text>
Apply? [y/n]The most recent proposal can sit several messages back in a discussion the user last touched yesterday, and the source may have grown since it was recorded; the confirmation shows both. Declining leaves everything as it was, review still open. The standard --confirm / --no-confirm / --yes flags apply.
--abort does not prompt. It writes nothing to the source and archives rather than deletes, so an accidental abort loses nothing that matters.
Deleting or archiving the source while a review is open is not blocked. --abort always works; it touches only the compaction conversation. --continue against an archived source applies normally. --continue against a deleted source fails: the source conversation no longer exists, and the error says to close the review with --abort.
Nothing is written to the source conversation until a confirmed --continue. Abandoning a review costs the compaction conversation and the tokens spent in it, and leaves the source conversation untouched.
--continue is idempotent. Apply comes before archive, so a failure between the two leaves the compaction applied and the review still open. A rerun detects that state — the source's most recent Compaction equals the proposal, and an open review means nothing else can have written it (see Compaction of the source is blocked) — and skips the prompt and the apply, finishing only the archive and the switch back.
Raw events and prior compactions
Raw events are preferred, and size decides. At fork time, JP estimates whether the source's raw events fit the review model's context window. When they fit, the fork drops the copied compaction events: the review model sees every original turn, and earlier summaries play no part in the review. When they do not fit, the fork keeps them: the review model sees earlier turns as their stored summaries, and a summary negotiated over such turns is built from those summaries, because the original messages cannot be loaded.
The step 1 summarizer follows the same rule: it reads the raw events of its range when they fit the summary model's window, and the projected view when they do not. A source with no prior compactions has nothing to drop or keep; both readings are the same.
Configuration
# Defaults to assistant.model.
[conversation.compaction.review.model]
id = "anthropic/claude-sonnet-4-5"model is a table mirroring assistant.model, the shape the rules' summary.model already uses.
Review is a multi-turn discussion with tool calls. The cheap one-shot model a rule may name under summary.model is a poor fit, so the review model defaults to assistant.model rather than to the rule's summary model. The setting resolves against the compaction conversation's own configuration, which the fork carries over from the source, and is applied through the fork-time configuration entry; --model overrides it.
Drawbacks
Three commands instead of one. Non-interactive
jp conversation compactremains a single invocation. Review costs a compaction conversation, a chat, and a finalize.Archive growth. Every review leaves an archived conversation carrying a full copy of the source conversation's events. Archiving keeps them out of the default listing, but the storage cost is real and grows with each review. Deleting is the user's call, because the compaction conversation is the record of why the summary says what it says.
Token cost. The compaction conversation carries the whole source conversation, and every review turn resends it. Prompt caching absorbs most of this, but a long review of a long conversation is not cheap.
A new built-in tool. JP has one today. Adding a second sets a precedent, and the model can forget to call it.
The opening proposal is fabricated. The review opens with a tool call the model never made. Models handle found history fine in practice, but the compaction conversation is not a faithful transcript of what the model did.
The model advises on its own summary. It has an incentive to defend the draft it wrote. The user decides, but the advice is not neutral.
Alternatives
Type the range, read the summary from the last message
--continue --keep-last 3 takes the range from flags and the summary from the model's final message. Rejected: after free chat the last message is usually acknowledgement, not the summary. The user also transcribes a range by hand, and nothing checks that it matches the range the summary was written for.
Ask the model to restate the proposal at finalize time
--continue sends one more turn asking for the proposal in a fixed shape, using the structured output JP already has, and prints it for confirmation. Rejected, narrowly. It needs no new tool and the confirmation step catches drift, but it spends a model call on every finalize, and no proposal exists as a distinct artifact during the chat, so revisions cannot be shown as they happen.
Review inside the source conversation
Hold the discussion in the source conversation, then extend the compaction range to cover the review turns so the model never sees them. Rejected on four counts. compact --reset restores the raw events and resurrects the discussion. Turn counts shift, silently changing what --keep-last 3, --last N, and RFD 064's range arithmetic mean. A later summary reads raw events and would summarize the meta-discussion as work. conversation print and conversation grep surface it forever.
An ephemeral review session inside one invocation
Run the review as an inquiry loop against the throwaway stream generate_summary already builds. Rejected: it needs a durable draft artifact to survive the process, and with it a rule for what happens when the conversation grows and the range resolves differently. A compaction conversation is durable for free.
A conversation attachment scheme
Give the compaction conversation read access to the source through a new conversation: attachment handler. Rejected: forking already copies the content. The attachment scheme may be worth having for other reasons, but this feature does not need it.
Non-Goals
Reviewing mechanical compaction on its own. Stripping reasoning and tool calls is deterministic and already previewable with
--dry-run. The policies are negotiable inside a review because they are fields of the proposal, but a review is not worth starting for them alone.Multiple proposals per review. One review produces one
Compaction. Multi-rule configurations are served by non-interactivecompact.Review in
jp query --compact. That path compacts silently before querying. Interactive review there is a mode collision.Automatic compaction review. Whatever RFD 064's follow-up on automatic compaction settles, an automatic trigger that stops to ask is not automatic.
Editing the stored event stream. Compaction remains an overlay.
Risks and Open Questions
- Does the model reliably call
propose_compaction? The review instructions have to make recording a revision habitual without the model spamming a call after every message. Needs tuning during implementation.
Implementation Plan
Phase 1: The proposal tool
Add the source field on the conversation record, and propose_compaction as a built-in tool, disabled by default. Its arguments mirror the Compaction fields RFD 064 defines. Every call is validated against the source conversation named by the conversation's source field: range within bounds, from_turn at most to_turn, policy values recognized, and the summary-overlap growth applied at once, so the recorded range is final. Arguments are 1-based positions, validated as such; the translation to the stored 0-based indices happens when --continue builds the Compaction (Phase 3), not in the tool. The tool result carries the validation error or the proposal rendered for display; a conversation that names no source is itself a validation error.
No command changes, and nothing enables the tool yet. Mergeable independently.
Phase 2: --interactive
Add the flag to jp conversation compact. Refuse when an open review already names the source. Fork the source conversation, inserting a turn marker at the start of each copied turn and recording the source ID in the field added in Phase 1. Drop the copied compaction events when the source's raw events fit the review model's context window; keep them when they do not, and feed the step 1 summarizer the projected view in that case. Reject --reasoning, --tools, --dry-run, and --reset alongside --interactive. Write the fork-time configuration entry: inherited tools off, propose_compaction on, review instructions, review model. Run the existing summarizer once, its range from the flags when given, otherwise the first configured rule with a summary section, otherwise the built-in default; write its draft as the opening proposal, activate the compaction conversation, and print instructions. Block compaction writes to the source while the review is open: non-interactive jp conversation compact (including --reset) and jp query --compact refuse when an open review names the conversation, reusing the open-review scan.
Depends on Phase 1.
Phase 3: --continue and --abort
Add both flags. Neither accepts a conversation ID or range flags; both require the active conversation to be a compaction conversation. --continue resolves the source conversation from the active compaction conversation's source field, locks both, reads the most recent propose_compaction call, re-validates it, prints it, and asks for confirmation (the shared --confirm / --no-confirm / --yes flags apply). On yes it builds and appends the Compaction, archives the compaction conversation, and reactivates the source. --abort archives the compaction conversation and reactivates the source without touching the source's event stream, and without prompting.
Archiving is the last step of --continue: a failure to archive must not leave the compaction unapplied. A rerun after such a failure finds the proposal already applied as the source's most recent Compaction, skips the confirmation and the apply, and completes the archive and reactivation. A deleted source makes --continue fail with a pointer to --abort; an archived source is applied to normally.
Depends on Phase 2.
Phase 4: Configuration
Add conversation.compaction.review.model, defaulting to assistant.model, applied through the fork-time configuration entry, and make --model override it the way it overrides rule summary models today.
Depends on Phase 2. Mergeable separately from Phase 3.