Stop an unrendered editor from confirming a launch

ConfirmInput's first poll can run before the TUI renders the pasted text. It
then finds an empty editor, falls through to the idle branch, and reports
confirmation=editor_cleared. The launch text sits unsent for the whole lease
while the coordinator believes the agent is working.

Run 5 died on exactly that: first_submit_at 21:13:19.570, confirmed_at
21:13:19.577. Seven milliseconds. The two launches that worked took ~500ms and
a second Enter, so the difference was scheduling luck.

An empty editor is only proof once it has held the text, or once it has stayed
empty past a settle window. Text seen and then gone still confirms at once, and
so do busy, blocked and queued. Waiting only happens in the never-observed
case, which is the one that cannot be told apart from a slow render.

With this, the failing case reaches the existing resubmit path instead: the
text appears, is recognised as unsubmitted, and Enter is resent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
This commit is contained in:
2026-08-28 01:23:48 +04:00
parent f860de4370
commit 32220d929b
2 changed files with 70 additions and 0 deletions
+46
View File
@@ -445,3 +445,49 @@ func TestMissingSessionIsRecognisedOnAnEmptyRunningServer(t *testing.T) {
}
}
}
// TestEmptyEditorIsNotProofBeforeItEverHeldTheText guards F33. The submit
// races the TUI's render: a poll 7ms after Enter found an empty editor, called
// it confirmation=editor_cleared, and the launch text then sat unsent for the
// whole lease. Run 5 died on exactly that. An editor that never held the text
// has to keep being observed, and once the text appears the resubmit path can
// do its job.
func TestEmptyEditorIsNotProofBeforeItEverHeldTheText(t *testing.T) {
dir := t.TempDir()
bin := filepath.Join(dir, "tmux")
calls := filepath.Join(dir, "calls")
// capture-pane reports an empty editor on the first two polls, exactly as a
// TUI that has not rendered the paste yet, then shows the submitted text.
script := `#!/bin/sh
cmd=""
for a in "$@"; do
case "$a" in capture-pane|has-session|display-message|send-keys) cmd=$a; break;; esac
done
case "$cmd" in
has-session) exit 0 ;;
send-keys) exit 0 ;;
display-message)
case "$*" in
*cursor_y*) printf '1\n' ;;
*) printf '0\tclaude\n' ;;
esac
exit 0 ;;
capture-pane)
n=$(cat ` + calls + ` 2>/dev/null || echo 0); n=$((n+1)); echo $n > ` + calls + `
if [ "$n" -le 2 ]; then printf '\xe2\x9d\xaf \n\n'; else printf '\xe2\x9d\xaf hello world\n\n'; fi
exit 0 ;;
esac
exit 0
`
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
b := &TmuxBackend{Binary: bin, LaunchConfirmTimeout: 300 * time.Millisecond, LaunchConfirmPoll: 5 * time.Millisecond}
_, err := b.ConfirmInput(context.Background(), Session{PaneID: "s:1.0"}, "hello world")
if err == nil {
t.Fatal("an editor still holding the submitted text was reported as confirmed")
}
if !errors.Is(err, ErrPromptNotSubmitted) {
t.Fatalf("err=%v, want ErrPromptNotSubmitted", err)
}
}