A query editor config retry reverts the draft on disk
- Status: Todo
- Kind: Bug
- Authors: jp
- Date: 2026-10-06
- Label: client=cli
- Label: package=jp_cli
When the "Active Configuration" block in QUERY_MESSAGE.md fails to parse, jp query reopens the editor on the user's text with the error annotated. If the user fixes it and the retry succeeds, QUERY_MESSAGE.md on disk is then reverted to what it held before the first editor session, or deleted if it did not exist yet.
The query is still sent correctly, because edit_query returns the text from the retry session. The damage shows up only when the turn then fails: the draft is the recovery copy of the request, and it no longer holds what the user wrote.
Reproduce
- Run a bare
jp qon a conversation with no draft. - Write a message, and break the TOML in the config block (for example, delete a closing quote). Save.
- The editor reopens with the parse error. Fix the TOML and save.
- Let the turn fail (for example, a model the provider rejects).
QUERY_MESSAGE.mdis gone, and the nextjp qopens an empty draft.
With an existing draft, step 5 instead shows the draft as it was before step 2.
Cause
The retry is a recursive call made while the outer frame's RevertFileGuard is still armed (crates/jp_cli/src/editor.rs, the Err(error) arm of the config parse in edit_query):
let (outcome, content, mut guard) = open(query_file_path.clone(), options, editor)?;
// ...
Err(error) => {
let error = error.to_string();
return edit_query(config, conversation_root, stream, "", editor, Some(&error));
}The inner call disarms its own guard, leaving the repaired text on disk. When it returns, the outer frame's guard drops and restores the file content from before the outer session, or removes the file if it did not exist.
Nothing writes the text back afterwards. Query::run_locked skips preserve_query_message_file for QuerySource::Editor, on the assumption that an editor-composed draft is already on disk (crates/jp_cli/src/cmd/query.rs, the comment above that call).
Fix direction
Keep one guard for the whole edit, across retries, and disarm it only when the final session parses. A cancel during a retry should still restore the file from before the first session, which is what the outer guard does today and what a plain "disarm before recursing" would lose.
Turning the recursion into a loop around open gives this naturally: the first guard is kept, each retry reopens the file without taking a new one, and success disarms it. RFD 080 already asks for this for ParseOutcome::Retry: "the protocol must preserve the edited file contents for the next editor invocation".
Tests should drive edit_query with a scripted editor that breaks the TOML in the first session and repairs it in the second, then assert the exact file content on disk after the call returns. Add a second case that cancels in the retry session and asserts the original content is restored.