Repository navigation
fix(trust-dialog): re-read a throttled trust-dialog scan instead of dropping it - #635
Open
JDProfresh wants to merge 1 commit into
Open
JDProfresh wants to merge 1 commit into
JDProfresh wants to merge 1 commit into
Conversation
…ropping it The scan must schedule its own next read rather than wait for PTY output, but a call inside the throttle was simply dropped. The dialog's own paint is often the last output a launching pane produces and lands inside that throttle, so the session sat on the dialog with no keystroke ever sent. A stopped session no longer scans, since the kill's own output kept it answering a dialog in a dead pane.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The trust-dialog scan must schedule its own next read rather than wait for PTY output (workspace-trust-dialog-auto-accept), but a call inside its 1.5 s throttle was simply dropped. The dialog's own paint is often the last output a launching pane produces and lands inside that throttle, so nothing ever read the screen again and the session sat on the dialog with no keystroke sent. This restores the invariant on the throttled path.
What I first went after did not reproduce. On 2026-10-06 (Claude Code 2.1.280) I saw launches log six
moving to the trust optionattempts and give up with the pane still onNo, exit, and I planned to spread the attempts out. On 2.1.294 that failure did not reproduce in 10 of 10 launches where the scan ran: the first arrow landed every time and the dialog cleared on the second keystroke. What did fail was the scan never firing at all, in 2 of 12 fresh launches. This PR fixes that and leaves the attempt spacing alone.Problem
On a fresh directory, a Claude session sometimes stays on the workspace-trust dialog with the cursor on
No, exitand the auto-accept never tries. On Claude Code 2.1.294 this was 2 of 12 fresh launches. The journal shows noAuto-accepting workspace trust dialogline at all for those sessions. The pane sat there for over 20 s until I deleted the session, and it would have sat until the 90 s window closed. A second, smaller issue showed up in the same logs. Once such a session is deleted, the kill's own output starts the scan again: it logs all six attempts against a dead pane over about 8 s, thendid not clear after retries.Cause
_maybeAcceptTrustDialog()runs fromonData, plus a one-shot timer after each keystroke it sends. Its throttle (TRUST_DIALOG_RETRY_MS) simply returned. With temporary debug logging around each return, a stuck launch looked like this:No keystroke had been sent, so no timer was pending, and nothing else calls the scan. CLAUDE.md already states the rule this breaks: "The scan must schedule its own next read (
_trustDialogTimer, cleared in_clearAllTimers()), not rely on PTY output." The keystroke path honours it; the throttled path did not. The post-delete attempts come from the same path:stop()clears_trustDialogTimer, but the scan does not check_isStopped. So the next output chunk re-arms the timer, and the scan keeps reading the buffer tail of a dead pane.Fix
_trustDialogTimer, for just past the end of the throttle. If a read is already scheduled (the post-keystroke timer, or an earlier deferral), that read covers it. The scan rate does not change, at most one read per throttle period while output flows, plus one trailing read after the output stops.stop().DELETE /api/sessions/:idreaches the session throughstop(), which sets_isStoppedfirst and then runs_clearAllTimers(), so a read deferred just before the delete is cancelled and any output after it is ignored._rescanTrustDialogIn()).All three guards stay as they were: the 90 s startup window, the two-marker
isTrustDialogScreen()match, and the 6-keystroke cap. So does the one-key-per-re-read rule, since a deferred read goes through the sametrustDialogNextKey()path.The six-attempt pattern from 2026-10-06 is exactly what a deleted stuck session logs (above), so I suspect that is what I saw then, but I cannot prove it now.
Tests
test/session-trust-dialog.test.ts:No, exit). Red without the deferral.stop()leaves no timer pending. Red without the_trustDialogTimerclear in_clearAllTimers().stop(), two scans leave no pending timer and log nothing. Red without the_isStoppedcheck. (My first version asserted on writes and passed without the guard, becausewriteViaMuxalready does nothing after stop, so it now asserts on the timer and the log, which is what the guard prevents.)The existing auto-accept tests (attempt cap, never Enter on
No, exit, stops once answered, window bound) pass unchanged. Full gate: 567 files passed, 11261 tests passed, 12 skipped.Verified
Claude Code 2.1.294, a throwaway Codeman instance (
CODEMAN_INSTANCE=w8trust, port 5187), one freshmktemp -d /tmp/...directory per launch, pane sampled once a second, journal read per session.Before (upstream master), observed: 12 launches. 10 cleared in two keystrokes, with the first arrow landing every time (so 0 of 10 showed the swallowed-arrow failure). 2 never got a keystroke. One of those was with debug logging on, which showed the single blank-screen read at +156 ms and nothing after it. Both then logged all six attempts after the session was deleted.
After (this branch, same temporary debug logging, removed before commit), observed: 12 launches, 12 cleared in exactly two keystrokes. In all 12 the first keystroke came from a deferred read about 1.75 s after start, i.e. the read that used to be dropped. There were no trust-dialog lines after any deletion.
Inferred, not measured: on a directory that is already trusted, the change costs at most one extra
capture-paneper burst of output in the first 90 s, the trailing read after the burst ends.Docs
The trust paragraph in CLAUDE.md and
docs/architecture-invariants.md#workspace-trust-dialog-auto-acceptnow say that a throttled read is deferred, never dropped, and that a stopped session never scans.Invariant touched: workspace-trust-dialog-auto-accept (restores "the scan must schedule its own next read" on the throttled path; the three guards are untouched).
Note
The companion PR #634 (holding idle while the dialog is on screen) touches neighbouring lines in
src/session.ts, the same test file and CLAUDE.md. A trial merge of the two branches is clean and the merged test file passes. The two fixes are independent. This one makes the dialog get answered. That one stops a waiting agent from prompting into the dialog before it is answered.