Skip to content

fix(runtime): answer startup terminal queries for background-created terminals - #22384

Merged
nwparker merged 3 commits into
stablyai:mainfrom
nwparker:background-terminal-query-replies
Sep 23, 2026
Merged

nwparker merged 3 commits into
stablyai:mainfrom
nwparker:background-terminal-query-replies

Conversation

@nwparker

Copy link
Copy Markdown
Contributor

ELI5

Some terminal programs ask the terminal a question when they start, such as "where is the cursor?", and wait for the answer. When Orca creates a terminal in the background (an orchestration worker, or orca terminal create) before you've opened any worktree, nobody answered. Muse Code gives up and quits when that happens. Orca now answers those questions until the terminal is shown on screen.

What Changed

  • Before: in a freshly started app still on its landing screen, orca orchestration worker-start --agent muse … failed with agent_readiness: timeout after 60 s. Muse printed nothing and exited 0.
  • After: the same command reaches ready / input_accepted in about 4.6 s. The worker runs its task and reports worker_done.
  • Mechanism:
    • Orca's main process answers terminal queries (cursor position, device attributes) only for a terminal the renderer has marked hidden (terminal-model-query-authority.ts).
    • Terminals the runtime creates in the background never got that mark. Before any worktree has been opened, no terminal view is mounted, so no one answered.
    • Background runtime spawns now start marked hidden (initiallyHidden), the same way the renderer's existing hidden-at-spawn path works:
      • New terminal-daemon sessions are marked before their first byte of output.
      • The mark is removed if the spawn fails, reattaches to an existing session, or is taken over by an existing agent session.
    • When you open the tab, the visible view clears the mark and restores from the saved screen, as before.
  • Code: the logic is in src/main/ipc/pty/runtime/spawn-hidden-delivery.ts. orca-runtime-create-terminal.ts passes the flag.

Why

We captured Muse 1.3 in a bare terminal. It sends colour, device-attribute and cursor-position queries, waits about 2 s on each unanswered cursor-position query, then exits 0 after about 6.4 s. Answering the cursor-position query is enough for it to start. Codex in the same bare terminal keeps running and just draws late, so Muse is the agent this breaks. But every agent in a background terminal got no answers, so the right fix is to answer, not a Muse-specific workaround. Reusing the existing hidden-at-spawn path means the handover when you open the tab behaves exactly like it already does for hidden panes the renderer creates.

Linked Issue

Found while validating #22383 (Muse orchestration workers, #19823).

Visual Proof

Before: fresh app on the landing screen (no worktree opened). The worker never came up, and worker-start returned:

Before: fresh app, worker missing

"state": "failed", "stage": "agent_readiness", "lastError": "timeout"

After: same fresh-app setup, still on the landing screen when the worker starts:

After: fresh app, landing screen before worker

worker-start returned "state": "ready", "stage": "input_accepted". Opening the worker's tab afterwards shows Muse rendered cleanly, with no stray ^[[1;1R reply characters. It did the task and sent worker_done, and the footer reads muse-spark-1.3 · minimal:

After: worker tab opened

Testing

  • I manually tested these changes locally

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

  • Manual test (macOS): in two separately launched dev apps, each with a fresh profile and still on the landing screen, worker-start --agent muse reached ready in about 4.6 s.

    • hello.txt was created and worker_done settled the task.
    • Opening the tab afterwards rendered cleanly, and typing worked.
  • Automated tests: src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts has 5 cases. One checks that main answers ESC[6n with ESC[1;1R for a terminal no renderer has opened, and it fails without the flag. The src/main/ipc suites (4,146 tests), tc:node and the changed-code quality gate pass.

AI Disclosure

Anthropic Claude assisted with implementation, validation, and this pull request description.

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Scope: only terminals the runtime creates in the background take this path. Terminals the renderer creates are unchanged.
  • SSH/WSL: remote terminals use the same runtime spawn, and query answers stay on the machine that owns the terminal. Nothing sent between client and host changes.
  • Not tested: opening the tab while the agent is still mid-task. It was only opened after the task finished, but the handover code is the existing one.

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 (tc:node and the ipc suites pass locally; CI covers the rest)

…terminals

A runtime-created terminal (orchestration worker-start, `orca terminal
create`) has no renderer pane until the user opens its tab. Main only
answers terminal queries for PTYs the renderer marked hidden, so on a
fresh app sitting on the landing screen nothing answered the agent's
startup cursor-position query. Muse waits ~2s per unanswered CPR and then
exits 0 with no output, which surfaced as `agent_readiness: timeout`.

Background runtime spawns now carry initiallyHidden, mirroring the
renderer's hidden-at-spawn path: fresh daemon sessions are marked before
byte zero, the committed id is marked and paced as backgrounded, and the
mark is released on failure, reattach, or adoption. A pane that later
mounts visible unmarks and restores from the model snapshot as before.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes — the full one-commit diff (f62c583), plus the runtime/CLI spawn path it threads through.

  • Background runtime spawns now start hidden — orca-runtime-create-terminal.ts spreads BACKGROUND_TERMINAL_SPAWN_FLAGS (initiallyHidden: true) into the background-branch ptyController.spawn, so main owns delivery and answers startup queries until a renderer view mounts.
  • New runtime hidden-delivery module (spawn-hidden-delivery.ts) — marks a fresh daemon session hidden before byte zero, re-affirms it after commit (with syncPtyBackgroundedDelivery(id, 'spawn') and closeStartupQueryAuthorityForPty), and releases the mark for reattach / stable-pane adoption / adopted agent session.
  • Controller plumbing — optional transitionSpawnHiddenRendererPtyDeliveryState / syncPtyBackgroundedDelivery deps wired in register-handlers.ts; preSpawnHiddenMarkId and the initiallyHidden arg added to spawn state and the controller contract.
  • Tests — new pty-runtime-hidden-at-spawn-mark.test.ts covers the pre-spawn mark, failure release, reattach skip, unflagged spawn, and an end-to-end ESC[6n → ESC[1;1R reply for a PTY with no renderer view; cli-terminal-create-host-session-binding.test.ts asserts initiallyHidden: true.

I verified the new suite and the related existing hidden-delivery suites pass, and pnpm run typecheck:node is clean. The deliberate divergence from the IPC twin (ipc/spawn-commit-persist.ts marks hidden even for reattach/adopted; this path releases instead) reads as correct — the runtime has no renderer request, so it must not hide a session that may already back a visible pane. Marking after sendPtySpawnedToRenderer is safe because main applies the mark synchronously in the same tick, before any renderer IPC can be handled.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 16 minutes.

Check out review usage here.

View limit details

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

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

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: bff86696-12cf-4bd5-ad61-4de11e135b1f

📥 Commits

Reviewing files that changed from the base of the PR and between d2eaedf and 1806b86.

📒 Files selected for processing (2)
  • src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts
  • src/main/ipc/pty/runtime/spawn-hidden-delivery.ts
📝 Walkthrough

Walkthrough

Background terminal creation now passes initiallyHidden to the PTY controller. The runtime controller marks eligible daemon sessions before spawn, updates hidden delivery state after commit, and clears the mark if spawning fails. Runtime-owned hidden marks survive renderer-scoped resets. Tests cover spawn outcomes, visibility handoff, reload behavior, and a startup cursor-position query answered by the main process.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to d2eae

Opening a terminal during startup can leave its output hidden until another visibility transition. Fix the handoff before merging, or explicitly accept this bounded risk.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.91% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 14 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: answering startup terminal queries for background-created terminals.
Description check ✅ Passed The description is complete and relevant. It covers the user impact, mechanism, rationale, linked issue, visual proof, testing, AI disclosure, scope, compatibility notes, and checklist status. It also…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: e47a890f-4593-4620-8516-eb0233ffec1a

📥 Commits

Reviewing files that changed from the base of the PR and between 9af6a3d and f62c583.

📒 Files selected for processing (11)
  • src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts
  • src/main/ipc/pty/register-handlers.ts
  • src/main/ipc/pty/runtime/controller-deps.ts
  • src/main/ipc/pty/runtime/spawn-execute.ts
  • src/main/ipc/pty/runtime/spawn-hidden-delivery.ts
  • src/main/ipc/pty/runtime/spawn-state.ts
  • src/main/ipc/pty/runtime/spawn.ts
  • src/main/runtime/cli-terminal-create-host-session-binding.test.ts
  • src/main/runtime/orca-runtime-create-terminal-dependencies.ts
  • src/main/runtime/orca-runtime-create-terminal.ts
  • src/main/runtime/runtime-pty-controller-contract.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

Comment thread src/main/ipc/pty/runtime/spawn-hidden-delivery.ts Outdated
Comment thread src/main/ipc/pty/runtime/spawn-hidden-delivery.ts
…eloads

A runtime background spawn has no renderer pane to report visibility or
re-mark it hidden, so it synced as foregrounded (no backpressure thinning)
and a reload/crash gate reset cleared its hidden mark, leaving startup
queries unanswered. Track runtime-owned hidden marks: they survive
renderer-scoped resets, count as known-hidden for backgrounded pacing
until a visible report, and are released by a renderer unmark or PTY
teardown.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Avoid re-marking a PTY that is already visible. · spawn-hidden-delivery.ts:29-43

src/main/ipc/pty/runtime/spawn-hidden-delivery.ts:29-43
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid re-marking a PTY that is already visible.

When an initially hidden daemon spawn becomes visible before commitRuntimeSpawnHiddenDelivery runs, the commit callback can reapply the hidden delivery gate. pty:setRendererPtyVisible updates visibleRendererPtys, but it does not clear the hidden gate. shouldDropHiddenRendererPtyData also ignores visible state, so output can be dropped from the opened terminal until another unhide transition occurs. Query authority is already closed by the visibility listener.

Suggested fix
 import { closeStartupQueryAuthorityForPty } from '../provider/registry'
+import { visibleRendererPtys } from '../delivery/visibility-state'
 import type { RuntimePtySpawnState } from './spawn-state'
@@
-  markRuntimeSpawnHidden(ctx, id)
-  if (ctx.preSpawnHiddenMarkId !== id) {
-    releaseRuntimeSpawnPreSpawnHiddenMark(ctx)
+  if (visibleRendererPtys.has(id)) {
+    releaseRuntimeSpawnPreSpawnHiddenMark(ctx)
+  } else {
+    markRuntimeSpawnHidden(ctx, id)
+    if (ctx.preSpawnHiddenMarkId !== id) {
+      releaseRuntimeSpawnPreSpawnHiddenMark(ctx)
+    }
   }
   ctx.deps.syncPtyBackgroundedDelivery?.(id, 'spawn')

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 6c92fa25-c5f6-4292-b864-c1e82e3c9b37

📥 Commits

Reviewing files that changed from the base of the PR and between f62c583 and d2eaedf.

📒 Files selected for processing (5)
  • src/main/ipc/pty-hidden-delivery-gate.test.ts
  • src/main/ipc/pty-hidden-delivery-gate.ts
  • src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts
  • src/main/ipc/pty/delivery/producer-sync.ts
  • src/main/ipc/pty/runtime/spawn-hidden-delivery.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts
  • src/main/ipc/pty/runtime/spawn-hidden-delivery.ts

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

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes — the new commit d2eaedf (range-diff against the prior pullfrog review at f62c583), plus the renderer visible-handoff path it depends on.

  • Tracked runtime-owned hidden marks — pty-hidden-delivery-gate.ts gains runtimeOwnedHiddenRendererPtys; resetRendererScopedHiddenPtyDeliveryState re-adds those PTYs after a reload/crash reset, so a pane-less background spawn keeps answering startup queries when no renderer party exists to re-mark it.
  • Released on unmark or teardown — unmarkHiddenRendererPty (renderer visible mount) and clearHiddenRendererPtyDeliveryState (wired into clearProviderPtyState) both drop the runtime-owned flag, so the mark cannot outlive the PTY or a renderer view that took ownership.
  • Paced as background — syncPtyBackgroundedDelivery now counts a runtime-owned PTY with no visible report as known-hidden, so the daemon pacer keeps thinning a background spawn's flood until a renderer reports visible.
  • Tests — the gate suite adds survival across a scoped reset plus release via unmark/teardown; the spawn suite adds reload survival, pacing-until-visible, and renderer-handover (no resurrection) cases. I ran these and the related hidden-delivery/lifecycle/claims suites (7 files, pass); pnpm run typecheck:node and pnpm run check:code-quality:changed are clean.

I traced the renderer handoff: a mounting visible pane's first syncHiddenRendererPtyDelivery calls declareRendererPtyDeliveryVisible, which sends setHiddenRendererPty(id, false), so a surviving runtime-owned mark is cleared as soon as a view owns it. That also means the visible report still wins for pacing, and the !visibleRendererPtys.has(id) guard keeps the two paths consistent.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@nwparker

Copy link
Copy Markdown
Contributor Author

Re CodeRabbit's outside-diff comment "Avoid re-marking a PTY that is already visible" (spawn-hidden-delivery.ts:29-43): valid, fixed in 1806b86.

A view can mount visible while the provider spawn is still pending. That mount sends visible:true and an unmark, which clears the hidden mark set before spawn. commitRuntimeSpawnHiddenDelivery then re-marked the PTY hidden, and the gate dropped the open view's output. The commit step now checks visibleRendererPtys.has(id). If the PTY is already visible, it releases any mark set before spawn, syncs pacing (which resolves to foreground), and does not mark it hidden again. The pre-spawn mark itself is only applied to freshly minted session ids, so no view can be visible at that point. Test: 'does not re-hide a PTY whose view mounted visible before spawn committed' asserts the PTY is not hidden and that its output reaches the renderer (pty:data). The test fails without the fix.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes — the new commit 1806b86 (range-diff against the prior pullfrog review at d2eaedf), plus the renderer visible-handoff path it depends on.

  • Skip re-hiding a visible-during-spawn PTY — commitRuntimeSpawnHiddenDelivery now returns early when visibleRendererPtys.has(id): it releases the pre-spawn mark and calls syncPtyBackgroundedDelivery(id, 'spawn') instead of re-marking hidden, so a pane that mounted visible before spawn committed does not have its bytes dropped.
  • Test — the runtime hidden-at-spawn suite gains a case that reports visible: true plus setHiddenRendererPty(false) while spawn() is gated, then asserts isHiddenRendererPty(result.id) is false and a subsequent chunk is delivered as pty:data. It fails without the guard.

The visible case skips closeStartupQueryAuthorityForPty, which is safe because the pty:setRendererPtyVisible listener already closed authority for that id. Ran pty-runtime-hidden-at-spawn-mark.test.ts: 9/9 pass.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@nwparker
nwparker merged commit 17ffbf3 into stablyai:main Sep 23, 2026
68 of 70 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

Development

Successfully merging this pull request may close these issues.

1 participant