Skip to content

fix(runtime): let a settled screen clear a blocked agent prompt the line tail kept after the TUI erased it - #33485

Merged
nwparker merged 4 commits into
mainfrom
nwparker/agint-agent-prompt-blocked-stale
Oct 25, 2026
Merged

nwparker merged 4 commits into
mainfrom
nwparker/agint-agent-prompt-blocked-stale

Conversation

@nwparker

@nwparker nwparker commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​71 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​71
Prod 8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​88 $\color{#cf222e}{\Huge{\mathbf{−}}}$​11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​77

ELI5

Orca kept refusing to send a prompt to an idle Codex pane with agent_prompt_blocked, even though the screen showed only the empty composer. It was reading a permission dialog that Codex had already erased: Orca's line-by-line copy of past output still had it. Now, if that copy says "blocked", Orca double-checks the live screen. The screen can lift the block only when it is fully up to date, the pane has gone quiet, no dialog wording appears anywhere on screen, and the agent's own ready composer is showing. Otherwise Orca behaves exactly as it does today.

Root cause

  • detectTerminalWaitBlockedReason reads snapshot.waitText, which is built from the retained line tail (buildTerminalWaitText). Codex draws and erases dialogs in place by cell diff (\x1b[row;colH, \x1b[J). A line tail can't undo that, so an answered dialog stays inside the 12-line window that blocked-text-layer.ts treats as live.
  • That text sets a waitBlockedAt stamp, and the stamp only lifts when a later hook, lifecycle event, or OSC 7501 status clears it. Codex 0.154, the reporter's version, never emits OSC 7501: strings on the @openai/codex@0.154.0-darwin-arm64 binary finds no 7501. The reporter's Stop hook also failed ("invalid stop hook JSON output"), so every later send was refused. Codex 0.164 sends 7501;state=done, which hides the bug only while hooks or 7501 keep working.

What changed

  • RuntimeTerminalAgentStatusQuery.readWaitText: the retained tail still decides. Only when the tail says blocked can readScreenClearingBlockedTail swap in the screen text, and only if all of these hold:
    • the emulator has applied every PTY chunk (headless.outputSequence === ptyOutputSequence) and passes the existing readRuledScreen checks: whole-screen model, drawn at the PTY's own size, no unrepainted reflow;
    • no output for TUI_IDLE_QUIESCENCE_MS (3 s), so a frame caught mid-paint can't clear;
    • findTerminalWaitBlockedSignal(screen, { wholeText: true }) finds nothing on any row (new option; the 12-line window stays for tails);
    • the agent's own screen rules return idle. For Codex that is codex-composer-ready; Codex dialogs replace the composer. Agents without screen rules never clear here.
  • A screen clear also drops the stale waitBlockedAt stamp on the PTY and its mirroring leaves. Without that, the paste's own echo in the composer would end the quiet window, the pre-Enter check would fall back to the tail, and the old stamp would refuse the send after the paste was already written. A newly painted blocker stamps again. None of the agents that can clear use agent-approval-prompt, the one reason that blocks without a stamp; only cursor.json does.
  • seedHeadlessTerminal: outputSequence is now set inside the write chain after the seed lands, so a daemon re-attach can't pass for a current grid before it's filled.
  • Every reader of waitText sees this: the send gate, agentWait in terminal show, getTerminalAgentStatus, and tui-idle's hook-lane blocked check. No wire or contract change.

How verified

  • Real CLI. Codex 0.154.0, pinned in a temp dir with an isolated CODEX_HOME, in a 120x40 PTY with -a on-request -s read-only. The real approval dialog ("Would you like to run the following command? … Press enter to confirm or esc to cancel") was answered with y, and I replayed the captured bytes through OrcaRuntimeService. On main, the idle pane shows agentWait prompt-text/agent-interactive-prompt and the send fails with agent_prompt_blocked, 0 writes. With this PR the prompt is written. While the dialog is still open, the send is refused.
  • Fixture replay over every src/main/runtime/__fixtures__ transcript with meta, in 64-char frames via replayTranscript:
    • Frames where the tail says blocked and this PR's screen check would clear it: 0 across all fixtures.
    • The first cut would have cleared 266 frames of freebuff-ready, frame 9 of antigravity-dialog-trust-workspace, and frame 558 of codex-0-160-0-plan-implement-menu. That last one is the half-painted plan menu.
    • My real Codex captures (0.154 approve, 0.164 approve, 0.164 reject) clear only on frames after the dialog was answered. The capture with the dialog still open clears on 0 frames.
  • Regression tests in agent-prompt-submission-runtime.test.ts:
    • Erased Codex dialog: refused while drawn and settled, accepted once erased and quiet, including when the paste echoes into the composer before Enter. Fails on main; with the echo it also fails on 5c06a06, which lacked the stamp clear.
    • OpenCode 2 layout (permission.tsx: ┃ border on every row, maxHeight: 15, "Permission required" on row 2): refused once settled. Passes on main, fails on the first cut of this PR.
  • pnpm tc passes. pnpm test src/main/runtime src/shared: 30640 passed. The 6 failures (editor-view-layout-apply, restored-editor-move-restart, sweep-terminal-placement, orca-runtime folder import) fail the same way on clean origin/main. check:code-quality:changed passes.
  • Not run: an Electron end-to-end check. It needs Orca's Codex hook to fail, because a working hook or 7501 done hides the stamp.

Risk

  • Never less safe than main for a dialog that is actually showing. The screen can only remove a block, and only if the whole grid has no blocker wording and the agent's own rules see its ready composer.
  • Only agents with screen rules can clear: Codex, Antigravity, Cline, Prime Agent, plus unknown panes through unknown-pane.json's Codex-header rule. Everyone else keeps main's behavior.
  • If a pane has no output timestamp, it counts as quiet. That is acceptable because a clear can only remove a block.
  • Because the seed now sets outputSequence only after it lands, Claude's interactive-wait screen read fails its sequence check in the window before the seed lands. A daemon-reattached Claude pane can therefore report the wait as unconfirmed (undefined) until the seed lands, instead of reading an empty grid.
  • A send within 3 s of the last output still uses main's tail behavior.

Credit

Supersedes #24308 by @innocarpe. It keeps that PR's core idea (check the current screen before refusing a send) and drops the stamp-clearing machinery. Co-authored in the commits.

Fixes #24187

… not an erased dialog in the tail

The retained line tail keeps a dialog a TUI has erased in place, so an idle
Codex composer kept refusing sends with agent_prompt_blocked whenever no hook
or OSC 7501 status cleared the stale stamp. Read blocked text off the live
emulator grid once it has applied every PTY chunk at the PTY's own size, and
keep the tail as the fallback otherwise.

Fixes #24187

Co-authored-by: Wooseong Kim <2222333+innocarpe@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a80e7196-773d-41d2-b1d7-7a15d5f87f7d
Commits

Reviewing files that changed from the base of the PR and between c75fa7c and 9c8aefd.

Files selected for processing: 9
  • src/main/runtime/agent-prompt-submission-runtime.test.ts
  • src/main/runtime/agent-state-rules/blocked-text-layer.ts
  • src/main/runtime/blocked-tail-screen-clear.ts
  • src/main/runtime/construction/runtime-construction-services.ts
  • src/main/runtime/construction/runtime-service-state.ts
  • src/main/runtime/runtime-terminal-agent-status-query.ts
  • src/main/runtime/terminal/output/terminal-buffer-operations.ts
  • src/main/runtime/terminal/output/terminal-visible-state.ts
  • src/main/runtime/terminal/wait/terminal-wait-blocked-checks.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

The runtime now checks caught-up terminal screen text when tail text indicates a blocked prompt. It accepts the screen only after the idle-quiescence interval and when the screen contains no blocked signal and agent rules classify it as idle. When the screen passes, the runtime clears wait-blocked stamps. Tests cover Codex approval and ready-composer states, and an OpenCode permission dialog taller than the terminal window.

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Low · up to 9c8ae

An erased approval dialog can still leave a pane appearing blocked when the screen is blank. This is a bounded remaining risk that merits owner awareness before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Linked Issues check Warning [#24187] has two groups of coding requirements. (1) Re-check blocked rules against the live screen when a send would be refused, so a stale wait does not persist on an idle pane. The PR meets this. `r… Add the matched blocked rule and the matched line to the agent_prompt_blocked error and to agentWait output, or have the [#24187] owner explicitly accept the live-screen re-check as the complete resolution for the diagnostic request.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly describes the primary runtime fix: allowing a settled screen to clear a stale blocked prompt.
Description check Passed The description is detailed and covers the user impact, root cause, implementation, testing, risks, and linked issue. It omits several template sections, including visual proof or an explicit N/A, com…
Out of Scope Changes check Passed All changed production files support the [#24187] fix. blocked-tail-screen-clear.ts and blocked-text-layer.ts (wholeText option) add the whole-screen blocker check. `runtime-terminal-agent-statu…
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 9 files.
Full details: Description check

Explanation

The description is detailed and covers the user impact, root cause, implementation, testing, risks, and linked issue. It omits several template sections, including visual proof or an explicit N/A, completed checklist items, and review notes, but it is mostly complete and relevant.

Full details: Out of Scope Changes check

Explanation

All changed production files support the [#24187] fix. blocked-tail-screen-clear.ts and blocked-text-layer.ts (wholeText option) add the whole-screen blocker check. runtime-terminal-agent-status-query.ts and runtime-service-state.ts wire the screen read into waitText. terminal-visible-state.ts and runtime-construction-services.ts expose the caught-up screen read. terminal-wait-blocked-checks.ts clears the stale waitBlockedAt stamp so the cleared state holds for the send. terminal-buffer-operations.ts sets outputSequence after the seed lands, which the caught-up check depends on. The test file adds Codex and OpenCode regression cases for this behavior. No unrelated behavior change is evident. The Claude seed timing effect is listed as a risk in the PR description. It is a side effect of the in-scope seed change, not an unrelated change.

Full details: Linked Issues check

Explanation

[#24187] has two groups of coding requirements. (1) Re-check blocked rules against the live screen when a send would be refused, so a stale wait does not persist on an idle pane. The PR meets this. readWaitText in runtime-terminal-agent-status-query.ts calls readScreenClearingBlockedTail only when the retained tail says blocked. blocked-tail-screen-clear.ts (screenClearsBlockedTail) clears the block only when no blocker appears on the whole screen and the agent's screen rules return idle. The PR also adds a regression test for an erased Codex dialog. (2) Include the matched rule and matched line in the agent_prompt_blocked error and in agentWait. The PR does not meet this. The changed files do not touch the send error or the agentWait shape, which stays {source, reason, since}. The PR description also says this part is not addressed. A supported stale-wait clear path exists only for a settled, quiet pane with a ready composer. Within 3 s of the last output, the tail still decides, so this path does not cover every case.

Resolution

Add the matched blocked rule and the matched line to the agent_prompt_blocked error and to agentWait output, or have the [#24187] owner explicitly accept the live-screen re-check as the complete resolution for the diagnostic request.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


Review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 83cb4f75-1b27-4507-9469-1fd367cd39f6
Commits

Reviewing files that changed from the base of the PR and between 4dcd6af and c75fa7c.

Files selected for processing: 5
  • src/main/runtime/agent-prompt-submission-runtime.test.ts
  • src/main/runtime/construction/runtime-construction-services.ts
  • src/main/runtime/construction/runtime-service-state.ts
  • src/main/runtime/runtime-terminal-agent-status-query.ts
  • src/main/runtime/terminal/output/terminal-visible-state.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

return null
}
const lines = this.readRuledScreen(ptyId)?.lines
return lines && lines.length > 0 ? lines.join('\n') : null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor · Functional Correctness · Quick win

🔎 Supported by static analysis

Script executed:

sed -n '175,220p' src/main/runtime/terminal/output/terminal-buffer-operations.ts
rg -n 'outputSequence:|outputSequence =|getPtyOutputSequence|seedHeadlessTerminal\\(' src/main/runtime/terminal src/main/runtime/terminal/output

Repository: stablyai/orca

Length of output: 2422


Script executed:

# Locate the initializer, the PTY sequence source, and all outputSequence writes/reads (fixed-string search, no regex).
rg -n -F -e 'createPtyHeadlessTerminalState' -e 'getPtyOutputSequence' -e 'outputSequence' -- src/main/runtime || test "$?" -eq 1

# Show the headless state factory with surrounding context.
rg -n -F -A 40 -e 'createPtyHeadlessTerminalState(' -- src/main/runtime || test "$?" -eq 1

# Show the reader as currently committed.
sed -n '170,220p' src/main/runtime/terminal/output/terminal-visible-state.ts

Repository: stablyai/orca

Length of output: 37362


Return '' for a painted blank grid, except while a seed write is pending.

If Codex erases a dialog and leaves a blank grid, readRuledScreen returns lines: []. This line returns null, so getSnapshot falls back to the retained tail. The tail can still contain the erased dialog and keep sends refused.

The fix needs a guard for the seed window. seedHeadlessTerminal stores the fresh emulator before its seed write applies, so the emulator is blank during that window. Moving the outputSequence assignment does not close the window. createPtyHeadlessTerminalState initializes outputSequence to 0, and a PTY with no output reports 0. In that case the sequences match before the seed write, so the reader would return '' for a blank grid that is not yet caught up. Track the pending seed instead of the sequence.

Declare pendingSeedStates beside the headlessTerminals field.

Suggested fix
--- a/src/main/runtime/terminal/output/terminal-buffer-operations.ts
+++ b/src/main/runtime/terminal/output/terminal-buffer-operations.ts
@@ -202,4 +202,5 @@
     const state = this.createPtyHeadlessTerminalState(ptyId, dims)
     state.outputSequence = this.getPtyOutputSequence(ptyId)
+    this.pendingSeedStates.set(ptyId, state)
     this.headlessTerminals.set(ptyId, state)
@@ -214,2 +215,5 @@
         await state.emulator.write(data)
+        if (this.pendingSeedStates.get(ptyId) === state) {
+          this.pendingSeedStates.delete(ptyId)
+        }
         if (this.headlessTerminals.get(ptyId) !== state) {
--- a/src/main/runtime/terminal/output/terminal-visible-state.ts
+++ b/src/main/runtime/terminal/output/terminal-visible-state.ts
@@ -177,8 +177,12 @@
   protected readCaughtUpTerminalScreenText(ptyId: string): string | null {
-    if (this.headlessTerminals.get(ptyId)?.outputSequence !== this.getPtyOutputSequence(ptyId)) {
+    const state = this.headlessTerminals.get(ptyId)
+    if (
+      !state ||
+      this.pendingSeedStates.get(ptyId) === state ||
+      state.outputSequence !== this.getPtyOutputSequence(ptyId)
+    ) {
       return null
     }
     const lines = this.readRuledScreen(ptyId)?.lines
-    return lines && lines.length > 0 ? lines.join('\n') : null
+    return lines ? lines.join('\n') : null
   }

@nwparker
nwparker marked this pull request as draft October 25, 2026 09:31
…r decide it alone

Review of the first cut: the blocked matcher scans the last 12 non-blank lines,
and on a grid every row counts, so a 15-row OpenCode 2 permission dialog read
as unblocked and the prompt + Enter approved it. The tail now decides as on
main; the screen may lift its verdict only when it is caught up, quiet for
TUI_IDLE_QUIESCENCE_MS, shows no blocker on any row, and the agent's own screen
rules see its ready composer. A daemon re-attach seed now marks the grid
current only after the seed lands.

Co-authored-by: Wooseong Kim <2222333+innocarpe@users.noreply.github.com>
@nwparker nwparker changed the title fix(runtime): judge a blocked agent prompt from the caught-up screen, not an erased dialog in the tail fix(runtime): let a settled screen clear a blocked agent prompt the line tail kept after the TUI erased it Oct 25, 2026
nwparker and others added 2 commits October 25, 2026 03:09
… dialog erased

Our own paste echo ended the quiet window, so the pre-Enter check fell back to
the tail and the old stamp refused the send with the paste written and no
Enter. A screen clear now also drops waitBlockedAt on the PTY and its mirroring
leaves; a newly painted blocker stamps again.

Co-authored-by: Wooseong Kim <2222333+innocarpe@users.noreply.github.com>
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.

[Bug]: agent_prompt_blocked persists on an idle Codex composer after #18817: the wait state isn't re-evaluated while the pane is quiet

1 participant