Skip to content

fix(runtime): fail session tab list with a retryable error instead of hanging when no graph publishes - #23356

Open
Meapri wants to merge 1 commit into
stablyai:mainfrom
Meapri:fix/session-tabs-inventory-wait-timeout
Open

Meapri wants to merge 1 commit into
stablyai:mainfrom
Meapri:fix/session-tabs-inventory-wait-timeout

Conversation

@Meapri

@Meapri Meapri commented Sep 27, 2026

Copy link
Copy Markdown

ELI5

When a client asks an Orca host for its list of tabs, the host waits until it has an up-to-date picture of its windows before it answers. On some headless hosts that picture never arrives, so the host kept the client waiting forever. Now it waits at most 30 seconds, then says "not available right now, try again" instead of never answering.

What Changed

  • Before: On a headless host whose window graph never gets published (orcad today, or orca serve after its hidden renderer died, as in [Bug]: headless serve — renderer process gone leaves graph 'unavailable' forever; terminal create/close from paired clients silently fail with runtime_unavailable #18604), session.tabs.listAll never answered: no error, no log. The inventory census behind session.tabs.subscribe hung the same way, so that stream never sent its first snapshot. Each stuck request also left a waiter registered on the host for as long as the connection lived.
  • After: If nothing is published within 30 seconds, the request fails with runtime_unavailable and its waiter is removed. That is the same error code the same host already returns for session.tabs.createTerminal in this state. When the graph does get published within the 30 seconds, nothing changes.
  • Mechanism: waitForSessionTabsInventoryPublication now starts a timer (SESSION_TABS_INVENTORY_PUBLICATION_WAIT_MS, 30s) next to its existing publish and abort listeners. Whichever fires first runs the shared cleanup, which clears the timer, drops the waiter and removes the abort listener. The timer is unref'd so it never keeps the process alive. Nothing else changes: the caller's retry loop, the authoritative labeling and the capability projection are untouched.

Why

The wait exists for a good reason. A client could prune tabs from a list the renderer hasn't published yet, so the host holds the answer until that list is real. What was missing was an upper bound. Alternatives considered:

30s is deliberately generous compared with a normal publish, so a slow but healthy startup still answers normally.

Linked Issue

Related to #17846 and #18604. This PR doesn't fully resolve either one:

Visual Proof

No visual change. This is host-side RPC timing: a request that used to hang now fails after 30s with an error clients already handle. Nothing is drawn differently.

Testing

  • Two new tests in src/main/runtime/session-tabs-inventory-publication.test.ts, using fake timers:

    • With no graph ever published, listAllMobileSessionTabsInventory rejects with runtime_unavailable after the wait, and no waiter is left behind.
    • Over the real RpcDispatcher with no client capabilities (the shape an older client sends), session.tabs.listAll answers { ok: false, error: { code: 'runtime_unavailable' } }.
  • Both new tests fail without the fix. I reverted the production file and kept only the exported constant so the test still imports. Both then time out because the call never settles. With the fix restored, they pass.

  • Existing tests: the inventory publication file (19 tests) plus every test under src/main/runtime/rpc/methods pass, 221 files and 2,220 tests.

  • pnpm tc, check:code-quality:changed, audit:anti-slop, oxlint and oxfmt on the changed files all pass.

  • Tested on macOS only. The change has no platform-specific code.

  • I manually tested these changes locally (covered by unit tests. I did not run orcad against a live client.)

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

AI Disclosure

Claude Opus 5.5 (via Claude Code) wrote the fix and the tests, directed by a human.

Review

AI code review summary:

  • Cross-platform: Only setTimeout/clearTimeout are used, with no platform branches. unref?.() is optional-chained, so it still works where a timer has no unref.
  • SSH / remote / local: The wait runs on the host that owns the runtime graph (the execution host), so an SSH or relay client gets the answer through the same RPC path as before. The error only means "this host has no published graph right now". It says nothing about whether any terminal or agent is running, so it doesn't blur the live / unverifiable / exited verdicts from docs/reference/ssh-execution-boundary.md.
  • Remote wire compatibility (old clients): No new field, opcode or error shape. The host now sends the standard error envelope on an existing method, with a code every client already knows. runtime_unavailable has been in the shared recoverable set (RECOVERABLE_CODES in src/shared/remote-runtime-client-error-classification.ts) since fix(remote-runtime): recover terminals after network loss #9774, and this host already returns it for createTerminal in this state. The error carries no tab list, so an old client cannot prune anything from it, which is the hazard the wait protects against. For example, the desktop's local inventory refresh throws on !response.ok and applies no snapshots. When the desktop loads tabs from a paired remote host, it already stops waiting for listAll after its own 15s timeout, so its visible behavior doesn't change. What changes is that the host no longer holds a waiter for it forever. Callers with no client-side timeout, and the census behind session.tabs.subscribe, now get an ending instead of an endless wait.
  • Agents / integrations: Agents and the PTY daemon are not touched. The only callers affected are tab-list requests made while the graph is unpublished.
  • Performance: One timer per waiting request, cleared as soon as the request settles. This also removes the waiters that used to pile up on a host that never publishes.
  • UI: N/A.
  • Security: No new inputs, IPC or permissions.

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

The 30s bound is a judgment call. It is longer than the 15s listAll timeout the desktop uses for paired remote hosts, on purpose, because the host-side bound is meant to catch a graph that never gets published, not to race the client's own timeout. I'm happy to change the value if you'd prefer something else.

Author

X: @mea_pri

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 (typecheck and the related tests pass locally, along with the changed-code lint gates. I left full pnpm lint, the full test suite and pnpm build to CI.)

🤖 Generated with Claude Code

…blishes

`session.tabs.listAll` (and the inventory census behind `session.tabs.subscribe`) withholds its answer until the renderer publishes the current graph, because a client without the authoritative-inventory capability would prune tabs from an unpublished list. On a host whose graph never publishes (orcad before its headless placeholder graph, or a stalled renderer) that wait had no bound, so a pre-capability client hung forever. The wait now gives up after 30s with `runtime_unavailable`, which every client already treats as retryable, and drops its waiter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 27, 2026 06:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 3229dbc4-097c-4a28-8e4c-de39c4c79694

📥 Commits

Reviewing files that changed from the base of the PR and between f5f537e and b4686f5.

📒 Files selected for processing (2)
  • src/main/runtime/orca-runtime-wait-for-session-tabs-inventory-publication.ts
  • src/main/runtime/session-tabs-inventory-publication.test.ts

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


📝 Walkthrough

Walkthrough

The inventory publication wait now accepts an optional timeout override and uses a 30-second default. If the inventory is not authoritative before the timeout, the wait rejects with runtime_unavailable and removes its waiter and abort listener. Tests cover the timeout error, waiter cleanup, and the error returned to a pre-capability client through RPC.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to b4686

Inventory requests now have a bounded wait and return a retryable error when publication does not arrive. No merge-blocking issue was found.

Security Architecture Review

Security architecture risk: 🔵 Low · up to b4686

Requests no longer wait indefinitely when the host has no published window graph. They receive an existing retryable error, and the review found no new route to unpublished tab data or broader authority. Surrounding security and deployment coverage remains incomplete.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The affected client-visible scope is the existing session-tabs list request and subscription census. On timeout, those paths fail instead of returning unpublished inventory.

Trust Boundaries and Controls

  • observed — The dispatcher supplies the existing request context, including cancellation and paired-device identity, while the runtime—not a request parameter—sets the wait limit and decides publication authority.

Resilience and Maintainability Implications

  • observed — Publish, abort, and timeout each invoke cleanup before settling the wait. Tests check that cancellation and timeout leave no registered publication waiter.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: replacing an indefinite session tab inventory wait with a retryable error when no graph publishes.
Description check ✅ Passed The description is complete and relevant. It explains the user impact, implementation, rationale, linked issues, testing, compatibility, and checklist status. It also explains that full lint, test, an…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
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.

@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 — bounded wait for the session-tab inventory publication, plus the two tests that pin it.

  • SESSION_TABS_INVENTORY_PUBLICATION_WAIT_MS — new exported 30_000 constant in src/main/runtime/orca-runtime-wait-for-session-tabs-inventory-publication.ts.
  • Timer in waitForSessionTabsInventoryPublication — a setTimeout (added with a defaultable timeoutMs param, unref'd) races the existing publish/abort listeners through the shared cleanup; the losing listeners are still removed exactly once, and a late onPublished after a timeout is a harmless no-op since the promise already settled.
  • Error mapping — the thrown new Error('runtime_unavailable') flows through mapRuntimeError's RUNTIME_PASSTHROUGH_CODES, the same path createTerminal already uses in this state, so no new wire shape or code is introduced.
  • Two fake-timer tests — one asserts the direct runtime call rejects runtime_unavailable and leaves no waiter; the other drives the real RpcDispatcher with no client capabilities and gets { ok: false, error: { code: 'runtime_unavailable' } }.

I verified the coverage is real, not theatre: the file passes (19 tests) with the fix, and both new tests time out (30s) when I removed the timer while keeping the constant exported, so they cannot pass with the bug present. Call-path check confirms only the no-signal / never-published case times out — an aborted request still rejects client_disconnected, and a graph published within the bound still resolves normally.

Two things worth recording, neither blocking. The timeoutMs parameter is currently never overridden by any caller, so it is a seam kept for testability. And the bound is per wait call inside the while (true) retry loop rather than a whole-request budget, so epoch churn could in principle exceed 30s in aggregate — not reachable in the never-publishes scenario this PR targets.

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

@Meapri

Meapri commented Oct 2, 2026

Copy link
Copy Markdown
Author

Hi @Jinwoo-H, gentle ping on this PR — all checks have passed and it's ready for merge. Whenever you have time, could you please take a look? Thank you!

This branch has not been deployed

No deployments
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