Skip to content

fix(automations): settle a run on the agent's own done when its screen never reads idle - #34096

Merged
nwparker merged 2 commits into
mainfrom
fix-34064-headless-tui-idle
Oct 26, 2026
Merged

nwparker merged 2 commits into
mainfrom
fix-34064-headless-tui-idle

Conversation

@nwparker

@nwparker nwparker commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​14 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​14
Prod 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​63 $\color{#cf222e}{\Huge{\mathbf{−}}}$​10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​53

ELI5

Automations that start Claude could finish their work and still never be marked done. Orca waited for the terminal screen to look idle, and on a headless host the screen sometimes never did. Now, if the agent itself reported that its turn ended after the run started, and that report has held for 30 seconds, the run is marked complete.

What Changed

Before: a terminal-session automation run completed only when tui-idle was satisfied and a done status row newer than the dispatch existed. If tui-idle never settled, the run stayed dispatched for 6 hours, ended dispatch_failed, and left its claude process running.

After: while the run observer waits for tui-idle, it re-checks every 30 seconds. If the pane's status rows show a turn-end done (not a session-start done and not a resume-only row) reported after the dispatch, the done has held for at least 30 seconds, and no row of the pane says the agent is working or waiting, the run completes. A screen that reads as blocked (for example Codex's plan menu after its Stop hook) still resolves the wait first, so those runs are still reported as blocked. Runs with no agent status at all keep the old behaviour.

Mechanism, from a captured Claude Code 2.1.310 session: when the terminal answers Claude's OSC 7501 query (the daemon does this), Claude stops putting ✳ in its title and reports rest only through OSC 7501. After a stream gap (the daemon thins hidden or background streams, which is the reporter's "hidden-delivery gate is dropping bytes" log), main ends the program activity and drops its at-rest done. A runtime restart has the same effect. The pane then has a glyph-free title such as Done, no title status, and no first-party rest, so tui-idle can never settle on that pane. I reproduced this in the runtime with the captured bytes: the wait settles before notePtyDataGap and times out after it.

Files: src/main/automations/runtime-terminal-run-observer.ts, src/main/automations/automation-run-agent-evidence.ts, and src/main/agent-turn-idle-verdict.ts. The last one now exports the existing "turn done since the prompt" rule so both checks use the same rule.

Why

The Stop hook is the agent's own statement that its turn ended. The screen is a heuristic that a lost stream, a restart, or a new idle screen can break. Letting a held hook done settle the run covers every way the screen can go unreadable, for every agent with hooks, not only this one Claude path. The 30-second hold means short races, such as Codex painting its plan menu about 750 ms after Stop or a Stop-hook continuation flipping the row back to working, are decided by the existing signals first.

I considered making Claude's hook done authoritative inside tui-idle itself. That would change behaviour for orchestration mail delivery and every orca terminal wait caller, and Claude was kept out of the hook lane on purpose (task wake-ups, #24878). This change is limited to automation runs. orca terminal wait --for tui-idle on a pane that lost its OSC 7501 state can still time out; that is a possible follow-up.

No wire or protocol change. The observer runs on the host that owns the automation, so SSH, paired clients and older clients are not affected. It does not depend on worktrees, so folder workspaces behave the same.

Linked Issue

Fixes #34064

Thanks to @spaceshipmike for the detailed report, especially the Stop/done evidence in last-status.json and the hidden-delivery log lines that pointed at the stream-gap path.

Visual Proof

N/A: no UI change. The change affects when a background automation run is recorded as complete.

Testing

  • Added an observer test: the pane never reads idle, and the agent reports done after dispatch. The run completes once the done has held 30 seconds, and not before. On the base commit this test hangs.

  • Replayed captured Claude Code 2.1.310 PTY bytes, with OSC 7501 answered, through the runtime's transcript harness. Before a data gap tui-idle is satisfied; after notePtyDataGap it times out. This was a probe and is not checked in.

  • pnpm tc, the full static job's lint, format, ast-grep, anti-slop, code-quality and ratchet checks, and every test file that imports a changed module. The only failures were root oxlint max-lines errors in files this PR does not touch, which are also on base.

  • Not run: a real headless orca serve under launchd on macOS.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

In the judged path the observer now re-arms its wait every 30 seconds instead of every 5 minutes. Each re-arm is one waiter registration and is cancelled cleanly on timeout.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…n never reads idle

A Claude Code that negotiates OSC 7501 drops its title glyph, so after a stream
gap or a runtime restart main holds no rest evidence for the finished pane and
tui-idle never settles. Runs then sat dispatched for 6h. A turn-end done
reported after dispatch that has stood for 30s, with no row of the pane saying
otherwise, now completes the run; a blocked screen still resolves first.

Fixes #34064
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 3 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

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: 62e426aa-0f6a-42a8-8f50-efef979305db
Commits

Reviewing files that changed from the base of the PR and between fe00bbf and fccfc85.

Files selected for processing: 4
  • src/main/agent-turn-idle-verdict.ts
  • src/main/automations/automation-run-agent-evidence.ts
  • src/main/automations/runtime-terminal-run-observer-evidence.test.ts
  • src/main/automations/runtime-terminal-run-observer.ts
Walkthrough

The change adds a predicate for qualifying agent done status rows and a helper that checks whether run evidence has remained done for a specified duration. When agent evidence is available, the terminal run observer uses a 30-second timeout for terminal idle waits. If the wait times out and the held-done condition is met, the observer returns completed with a terminal snapshot. A test covers a non-idle pane that reports done.

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Low · previous review fe00b

The completion path is mergeable, but the test should check closer to the 30-second hold so it catches premature completion.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: completing automation runs from held agent-reported done status when the terminal screen never becomes idle.
Description check Passed The description follows the repository template and explains the user impact, implementation, rationale, linked issue, testing, scope, compatibility considerations, and checklist status. It also recor…
Linked Issues check Passed For #34064, the observer now gives a run with agent evidence a 30-second tui-idle wait. On timeout, hasHeldRunTurnDone completes the run when a non-session turn-end done row is newer than dispat…
Out of Scope Changes check Passed The changed production files implement the completion path requested by #34064. The exported predicate removes duplicated turn-end filtering, hasHeldRunTurnDone isolates the evidence rule, and the o…
Full details: Description check

Explanation

The description follows the repository template and explains the user impact, implementation, rationale, linked issue, testing, scope, compatibility considerations, and checklist status. It also records that full local lint, typecheck, test, and build commands were not all run.

Full details: Linked Issues check

Explanation

For #34064, the observer now gives a run with agent evidence a 30-second tui-idle wait. On timeout, hasHeldRunTurnDone completes the run when a non-session turn-end done row is newer than dispatch and has held for 30 seconds. The check requires all non-session rows to remain done, so working or waiting evidence prevents completion. The observer still returns a blocked result when the terminal wait reports a blocked screen. Runs without agent evidence keep the previous behavior. The added observer test covers completion after the hold period and non-completion before it. The shared isTurnDoneSince predicate keeps the idle and observer checks consistent.

Full details: Out of Scope Changes check

Explanation

The changed production files implement the completion path requested by #34064. The exported predicate removes duplicated turn-end filtering, hasHeldRunTurnDone isolates the evidence rule, and the observer test verifies the new timing. The broader support for agents with status hooks remains within the stated completion behavior and does not alter tui-idle callers outside automation runs. No unrelated change is shown in the supplied pull request summary.

Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • 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.

🧹 Nitpick comments: 1
src/main/automations/runtime-terminal-run-observer-evidence.test.ts: 1

260-273: Trivial · Functional Correctness · Quick win

Assert before the 30-second hold, then confirm completion.

The test currently checks only 20 seconds after pane.report(). Extend that check to 29 seconds. Do not require the run to remain unsettled when the hold reaches 30 seconds, because completion is permitted at that boundary.

Suggested fix
-    await vi.advanceTimersByTimeAsync(20_000)
+    await vi.advanceTimersByTimeAsync(29_000)
     expect(run.settled).not.toHaveBeenCalled()

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: 23485c5c-6fa8-48fc-9b03-121cdcb4cf08
Commits

Reviewing files that changed from the base of the PR and between 2e67ba5 and fe00bbf.

Files selected for processing: 4
  • src/main/agent-turn-idle-verdict.ts
  • src/main/automations/automation-run-agent-evidence.ts
  • src/main/automations/runtime-terminal-run-observer-evidence.test.ts
  • src/main/automations/runtime-terminal-run-observer.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.

@nwparker
nwparker merged commit 7423c8c into main Oct 26, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant