Skip to content

fix(idle): hold idle while the workspace trust dialog is on screen - #634

Merged
Ark0N merged 1 commit into
Ark0N:masterfrom
JDProfresh:fix/idle-not-on-trust-dialog
Oct 16, 2026
Merged

Ark0N merged 1 commit into
Ark0N:masterfrom
JDProfresh:fix/idle-not-on-trust-dialog

Conversation

@JDProfresh

@JDProfresh JDProfresh commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Claude Code's workspace-trust dialog puts its ❯ on No, exit, and that glyph arms idle confirmation like the composer does. The dialog is quiet and has no working line, so a fresh case was concluded idle while the dialog was still up.

Problem

On a brand-new directory, GET /api/sessions/:id/wait?until=idle answers idle about 3.8 s after launch while the pane still shows the trust dialog with the cursor on No, exit. An agent that waits for idle and then sends its first prompt types into the dialog, and the prompt's Enter confirms No, exit. The CLI quits, the pane dies (Pane is dead (status 1)), and the send-and-wait reports exit. I hit this twice on 2026-10-06 with fresh /tmp dirs on Claude Code 2.1.280, and it reproduces on 2.1.294 (numbers below).

Cause

_detectInteractiveActivity() arms _confirmIdle() on any chunk that carries the CLI's promptGlyph. For Claude that is ❯, and the trust dialog draws one as its selection marker. _confirmIdle() then checks that the pane went quiet and that _probePaneWorking() finds no working line. The dialog passes both, so _concludeIdle() runs: the status flips to idle and the idle event fires. currentSignalFor() and the wait registry read those two, so every idle consumer (wait, send-and-wait, the default stop,idle,exit set) sees the same false idle. The auto-accept usually answers the dialog a second or two later, which is why this is a race, and why it is a dead pane every time when the accept is slow or never lands.

Fix

  • _probePaneWorking() already captures the pane at the moment idle is decided. It now also records whether that capture shows the dialog, using the existing isTrustDialogScreen() two-marker match. A failed capture keeps the last answer, since it proves nothing either way.
  • _confirmIdle() re-checks on PANE_PROBE_RECHECK_MS while the dialog is on screen, instead of concluding idle. This holds only inside the auto-accept's own startup window (TRUST_DIALOG_WINDOW_MS, 90 s), so a later screen that merely quotes the dialog (an agent reading session-trust-dialog.ts) cannot hold a session busy.
  • The status is left where it is (busy from spawn), not marked working: nothing is running. So wait?until=idle keeps waiting until the dialog is answered and the composer is up, and until=working does not fire on it.
  • startInteractive() clears the recorded verdict, so a restarted pane does not inherit the last pane's answer. This matters when the new pane's probe cannot replace that answer: the direct-PTY fallback has no pane to capture, and a failed capture keeps the last answer. Without the reset, a restart there after the dialog was seen would hold the new composer busy for the whole 90 s window.
  • One log line when the hold starts, so a session sitting on the dialog explains itself in the journal.

Because _concludeIdle() is the only place a Claude pane becomes idle (the launch settle in _settlePaneStartup() does not apply to a CLI that declares workDetect, and idle_prompt never fires on the dialog), gating _confirmIdle() covers every idle path.

Tests

test/session-trust-dialog.test.ts, new block "Session idle detection while the trust dialog is up", driving the real _detectInteractiveActivity() and _confirmIdle() against a scripted pane (the verbatim 2.1.252 capture already in the file):

  • the dialog's own ❯ arms the check, and after 30 s the session is still busy with no idle event;
  • once the screen shows the composer, it goes idle by itself (no new output needed) and emits exactly one idle;
  • a capture that fails after the dialog was seen keeps holding;
  • a dialog-looking screen outside the startup window still goes idle;
  • a restarted pane does not inherit the last pane's verdict: a real direct-PTY startInteractive() (pty.spawn stubbed with a harmless process) after the flag was set by an earlier pane, and the new pane's ❯ still goes idle. The direct-PTY fallback has no pane to capture, so nothing would ever overwrite the stale verdict there (a failed capture keeps it the same way), and without the reset the new pane would read busy for its whole 90 s window.

I removed each guard and watched its test go red: the _confirmIdle() hold (first two red), the null-capture rule (third red), the window bound (fourth red), the startInteractive() reset (fifth red, expected 'busy' to be 'idle'). Full gate: 567 files passed, 11262 tests passed, 12 skipped, on the tree before the restart test was added. With it, the trust, activity and awaiting-workers suites pass, 99 of 99, three runs.

Verified

Claude Code 2.1.294, a throwaway Codeman instance from this branch's base (CODEMAN_INSTANCE=w8trust, port 5187), one fresh mktemp -d /tmp/... directory per launch, POST /api/sessions + POST .../interactive, then GET .../wait?until=idle while sampling tmux capture-pane once a second.

Before (upstream master), observed:

  • 12 launches, wait only: 6 answered idle at 3.8 s with the pane still on ❯ No, exit. In 2 of those the dialog was never answered at all (a separate auto-accept bug, sent in its own PR), so the session read idle for its whole life while parked on the dialog.
  • 4 launches that send a one-line prompt the moment the wait returns, like an agent would: 3 of 4 had the wait answer idle at 3.8 s on ❯ No, exit. The prompt's send-and-wait then resolved exit 3 s later, and the pane read Pane is dead (status 1). The fourth happened to go idle after the accept and survived.

After (this branch), observed:

  • 5 launches with the same send-after-idle script: the journal logged Holding idle while the workspace trust dialog is on screen, the wait answered idle at 7.8 to 8.8 s with no dialog on screen, and all 5 panes were alive afterwards (pane_dead=0, composer up).

Inferred, not measured: idle now lands up to one PANE_PROBE_RECHECK_MS (5 s) after the dialog is answered, because the hold re-checks on that cadence. On a directory that is already trusted nothing changes, since the dialog never appears.

Docs

The idle-detection paragraph in CLAUDE.md and docs/architecture-invariants.md#idle-detection-composer-glyph-and-working-line describe the hold. docs/api-reference.md, docs/extending-codeman.md and docs/wiki/Driving-Codeman-From-An-Agent.md said a prompt sent into the dialog just sits there while the startup idle resolves the wait. Measured, it confirms No, exit and kills the pane, so I corrected those sentences. They still say idle is not a readiness signal.

Invariants touched: idle-detection-composer-glyph-and-working-line, workspace-trust-dialog-auto-accept (reuses its window and two-marker match; the three auto-accept guards are untouched), agent-wait-primitives (behaviour only, no route change).

Note

The companion PR #635 (re-reading a throttled trust-dialog scan) touches neighbouring lines in src/session.ts, the same test file and the trust paragraph in CLAUDE.md. A trial merge of the two branches is clean, and the merged test file passes.

The dialog's own prompt glyph armed idle confirmation and the quiet dialog passed it, so a fresh case reported idle about 4 s in. An agent that waited for idle and then prompted typed into the dialog, and its Enter confirmed No, exit and killed the pane.
@Ark0N
Ark0N merged commit 9aa3083 into Ark0N:master Oct 16, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 16, 2026
feat(codex): raise codex approval, question and trust dialogs as BLOCKED

Conflict resolved against #634 in src/session.ts _probePaneWorking():
#624's assignment (a dialog on screen is never work) is kept, and #634's
trust-dialog verdict block (log the hold once, remember the reading)
follows it, before _readWatching().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 16, 2026
One release changeset for #616, #617, #621, #622, #623, #624, #625, #628, #632, #634, #635 and #642. The changesets carried by #616, #617 and #642 are folded into it and removed (#617's was minor; 1.41.2 is a patch release).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Merged and shipped in 1.42.0. Thanks @JDProfresh!

The before/after numbers on brand-new folders made this easy to trust, and every guard has a test that goes red without it, the restarted-pane case included. I re-ran it with Claude Code 2.1.301 in fresh folders: on master an idle wait answered while the dialog was still up in 4 of 6 launches, with this PR it held every time. Together with #635 the dialog is usually gone before the hold even matters, so idle lands about as fast as in a trusted folder.

@JDProfresh
JDProfresh deleted the fix/idle-not-on-trust-dialog branch October 16, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants