Skip to content

Hide native chat UI until terminal lease is ready - #10144

Open
AmethystLiang wants to merge 4 commits into
mainfrom
on-mobile-no-chat-ui-if-terminal-not-ready
Open

AmethystLiang wants to merge 4 commits into
mainfrom
on-mobile-no-chat-ui-if-terminal-not-ready

Conversation

@AmethystLiang

Copy link
Copy Markdown
Contributor

Summary

Hides the native chat UI on mobile until the terminal input lease is ready, preventing premature message sending. Introduces lease-only subscriptions to separate input-floor authority from terminal-output view authority, reducing unnecessary state synchronization for chat-only flows.

Key Changes

  • Mobile chat readiness gating: Chat composer is disabled and delayed placeholder shown until lease acknowledgement from the host, with configurable placeholder timing
  • Lease-only subscriptions: New handleMobileLeaseSubscribe RPC path registers input authority without terminal output view subscription or query authority
  • Terminal stream retry mechanism: Covered chat subscriptions use exponential backoff (250ms–4s) to recover from transient failures while respecting chat visibility state
  • Provider session publishing: Hook-only provider session changes republish mobile tabs with version increment; identity flows through to mobile clients from main hook cache
  • Git config deduplication: appendGitConfigEnv now skips entries already effective in the environment to avoid redundant configuration
  • Emulator desktop runtime build: New script builds current worktree runtime for mobile pairing, with environment isolation for transcript home
  • Transcript home isolation: Agent transcripts from login shells can be stored outside the isolated emulator runtime home via ORCA_NATIVE_CHAT_TRANSCRIPT_HOME_DIR

Testing

  • Added tests for lease-ready state transition and placeholder delay timing
  • New tests for terminal stream controller retry backoff and chat closure cancellation
  • Tests for composer disable state discrimination (send vs. edit)
  • Mobile tab republish on hook-only provider session changes
  • Lease-only subscribe flow with timing metrics and unavailability reporting
  • Git config deduplication detection and append ordering
  • Emulator desktop runtime build and CLI override paths

Implement retry logic with exponential backoff for covered terminal streams
that terminate. Separate lease-only subscriptions (chat input) from full
subscriptions (terminal output), allowing chat to wait for send authority
without blocking terminal rendering. Rename composer's `disabled` prop to
`sendDisabled` to distinguish input-lock from send-lock. Track readiness
timing for diagnostics to measure latency across server, PTY wait, and
lease registration stages.
Mobile pairing runtime uses isolated HOME for process isolation. Add ORCA_NATIVE_CHAT_TRANSCRIPT_HOME_DIR to let login-shell agents write transcripts to the real user's home instead.
- Enable mobile tabs to receive provider session updates from main process hook
  cache without requiring terminal mutations (republish on hook-only changes).
- Build the current worktree's desktop runtime when starting mobile pairing
  to prevent protocol mismatches from using an installed app version.
- Add process group isolation on non-Windows platforms for reliable
  cleanup of the headless pairing runtime and its descendants.
- appendGitConfigEnv now skips entries already present with the same value
- hasRemoteTerminalViewSubscriber excludes lease-only subscribers
- lease-only heirs receive fit-hold ownership for restore on last-leave
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The changes add controller-based mobile terminal stream lifecycle management with retry scheduling, lease readiness helpers, subscription parameter construction, and expanded diagnostics. Composer send locking is separated from delayed placeholder updates. Emulator pairing gains desktop runtime preparation, isolated environment handling, transcript-home configuration, and process-group shutdown. Runtime lease-only subscriptions now report readiness timing and affect authority and fit restoration state. Mobile agent status snapshots incorporate hook-cache updates, while transcript resolution and Git credential environment handling are expanded.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary and testing, but it omits required screenshots, AI review report, security audit, notes, and the detailed checklist. Add the missing template sections: Screenshots, a completed Testing checklist, AI Review Report, Security Audit, and Notes, with cross-platform review details.
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: hiding native chat UI until the terminal lease is ready.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

🧹 Nitpick comments (3)
src/main/native-chat/session-file-resolver.test.ts (1)

126-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the Codex transcript-home fallback.

This test verifies Claude and Grok only, but Lines 33-36 also move Codex’s ~/.codex/sessions fallback under ORCA_NATIVE_CHAT_TRANSCRIPT_HOME_DIR. Add a Codex fixture/assertion with CODEX_HOME unset.

src/main/git/runner.test.ts (1)

195-227: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a regression test for case-insensitive duplicates.

The duplicate test uses identical casing, while the mixed-case test changes the value and would also pass with append-only behavior. Add a case such as existing CREDENTIAL.INTERACTIVE=false plus incoming credential.interactive=false, asserting that the count and indexed entries remain unchanged.

mobile/src/session/mobile-native-chat-terminal-subscribe-source.test.ts (1)

1-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Source-text matching makes this test brittle to reformatting.

Asserting on exact substrings/whitespace of the raw .tsx source ('covered,\n viewport: viewportRef.current') will break on any prettier/indentation change even when behavior is unchanged, and it can't catch behavior regressions that preserve the same text. Consider asserting on the actual RPC params passed to a mocked client.subscribe (e.g. via a small render/hook harness) instead of string-matching source.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 37317a08-d6aa-4959-9b7e-cfb5b584b6ba

📥 Commits

Reviewing files that changed from the base of the PR and between 7ab6014 and eb35b4c.

📒 Files selected for processing (29)
  • mobile/app/h/[hostId]/session/[worktreeId].tsx
  • mobile/scripts/start-emulator-desktop-runtime.mjs
  • mobile/scripts/start-emulator-pairing-runtime.mjs
  • mobile/scripts/start-emulator.mjs
  • mobile/src/session/MobileNativeChatComposer.test.ts
  • mobile/src/session/MobileNativeChatComposer.tsx
  • mobile/src/session/MobileNativeChatView.test.ts
  • mobile/src/session/MobileNativeChatView.tsx
  • mobile/src/session/mobile-native-chat-terminal-stream.test.ts
  • mobile/src/session/mobile-native-chat-terminal-stream.ts
  • mobile/src/session/mobile-native-chat-terminal-subscribe-source.test.ts
  • mobile/src/session/mobile-terminal-diagnostics.test.ts
  • mobile/src/session/mobile-terminal-diagnostics.ts
  • mobile/src/session/use-mobile-native-chat-terminal-stream.test.ts
  • mobile/src/session/use-mobile-native-chat-terminal-stream.ts
  • mobile/src/start-emulator-desktop-runtime.test.ts
  • mobile/src/start-emulator-pairing-runtime.test.ts
  • src/main/git/runner.test.ts
  • src/main/index.ts
  • src/main/native-chat/session-file-resolver.test.ts
  • src/main/native-chat/session-file-resolver.ts
  • src/main/native-chat/transcript-watch-liveness.test.ts
  • src/main/runtime/mobile-subscribe-integration.test.ts
  • src/main/runtime/orca-runtime.test.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/terminal.ts
  • src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection.test.ts
  • src/shared/git-credential-prompt-env.ts

Comment on lines +1376 to 1386
const terminateStream = () =>
chatStreamRef.current?.terminateStream(handle, unsubscribeTerminalRef.current)
const unsub = subscribeMobileTerminalSafely(
client,
{
nativeChatTerminalStream.buildMobileNativeChatTerminalSubscribeParams({
terminal: handle,
client: { id: deviceTokenRef.current!, type: 'mobile' as const },
viewport: nativeChatTerminalStream.mobileNativeChatSubscribeViewport(
covered,
viewportRef.current
),
capabilities: nativeChatTerminalStream.mobileNativeChatTerminalCapabilities(covered)
},
clientId: deviceTokenRef.current!,
covered,
viewport: viewportRef.current
}),
(result) => {

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n "deviceTokenRef.current" "mobile/app/h/[hostId]/session/[worktreeId].tsx"

Repository: stablyai/orca

Length of output: 1075


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="mobile/app/h/[hostId]/session/[worktreeId].tsx"

printf '--- Around lines 1340-1410 ---\n'
sed -n '1340,1410p' "$FILE"

printf '\n--- References around other occurrences ---\n'
for line in 1570 2345 2975 3010 3050 3315 3415 3795; do
  start=$((line-15)); end=$((line+25))
  printf '\n### lines %d-%d\n' "$start" "$end"
  sed -n "${start},${end}p" "$FILE"
done

printf '\n--- Search for buildMobileNativeChatTerminalSubscribeParams and subscribeMobileTerminalSafely ---\n'
rg -n "buildMobileNativeChatTerminalSubscribeParams|subscribeMobileTerminalSafely|subscribeTerminal|mobileNativeChatTerminal" "$FILE"

Repository: stablyai/orca

Length of output: 242


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="mobile/app/h/[hostId]/session/[worktreeId].tsx"

if [ ! -f "$FILE" ]; then
  printf 'File not found: %s\n' "$FILE"
  git ls-files | sed -n '/mobile\/app\/h\/\[hostId\]\/session\/\[worktreeId\].tsx/p' || true
  exit 1
fi

printf '%s\n' '--- Around lines 1340-1410 ---'
sed -n '1340,1410p' "$FILE"

printf '%s\n' ''
printf '%s\n' '--- References around other occurrences ---'
for line in 1570 2345 2975 3010 3050 3315 3415 3795; do
  start=$((line-15))
  end=$((line+25))
  printf '%s\n' "$line"
  sed -n "${start},${end}p" "$FILE"
done

printf '%s\n' ''
printf '%s\n' '--- Search for buildMobileNativeChatTerminalSubscribeParams and subscribeMobileTerminalSafely ---'
rg -n "buildMobileNativeChatTerminalSubscribeParams|subscribeMobileTerminalSafely|subscribeTerminal|mobileNativeChatTerminal" "$FILE"

Repository: stablyai/orca

Length of output: 16463


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="mobile/app/h/[hostId]/session/[worktreeId].tsx"

printf '%s\n' '--- subscribeMobileTerminalSafely call and result handling ---'
sed -n '1346,1510p' "$FILE"

printf '%s\n' ''
printf '%s\n' '--- locate imported module files ---'
rg -n "function( async)? subscribeMobileTerminalSafely|buildMobileNativeChatTerminalSubscribeParams|interface .*Client|type .*Client|client:" mobile/src -S -g '.*\.(ts|tsx)$' | head -80 || true

Repository: stablyai/orca

Length of output: 7997


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Locate mobile terminal stream module files ---'
fd -i 'mobile-terminal-stream-subscribe|mobile.*terminal.*stream' mobile/app mobile/src -t f | sed -n '1,80p'

printf '%s\n' ''
printf '%s\n' '--- Search token/client identity construction ---'
rg -n "mobileNativeChatTerminalSubscribeParams|buildMobileNativeChatTerminalSubscribeParams|subscribeMobileTerminalSafely|NativeChatTerminalSubscribeParams|deviceToken|client:" mobile -S -g '*.ts' -g '*.tsx' | sed -n '1,220p'

Repository: stablyai/orca

Length of output: 21505


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="mobile/src/session/mobile-terminal-stream-subscribe.ts"
NATIVE="mobile/src/session/mobile-native-chat-terminal-stream.ts"

printf '%s\n' '--- mobile-terminal-stream-subscribe.ts ---'
cat -n "$FILE"

printf '%s\n' ''
printf '%s\n' '--- relevant mobile-native-chat-terminal-stream.ts definitions ---'
sed -n '1,180p' "$NATIVE" | cat -n

printf '%s\n' ''
printf '%s\n' '--- tests for params/client semantics ---'
sed -n '1,130p' mobile/src/session/mobile-terminal-stream-subscribe.test.ts | cat -n
sed -n '1,120p' mobile/src/session/mobile-native-chat-terminal-stream.test.ts | cat -n

Repository: stablyai/orca

Length of output: 10393


Guard deviceTokenRef.current before subscribing the mobile terminal stream.

subscribeToTerminal() already checks client, but it can still build and send terminal.subscribe before loadHosts() populates deviceTokenRef.current. Building params with client: { id: undefined, type: 'mobile' } does not match the guarded RPC-client pattern used for terminal.send, so send only after deviceTokenRef.current is set and return early with subscribingHandlesRef.current.delete(handle).

Comment on lines 1386 to 1400
(result) => {
if (subscribeSeqRef.current.get(handle) !== seq) {
return
}
const data = result as Record<string, unknown>
diagnostics.firstStreamEvent(handle, seq, data.type)
diagnostics.firstStreamEvent(handle, seq, data)
if (data.type === 'end' || data.type === 'error') {
unsubscribeTerminalRef.current(handle)
terminateStream()
return
}
if (data.type === 'subscribed') {
if (nativeChatTerminalStream.isMobileNativeChatLeaseReady(covered, data)) {
markNativeChatInputLeaseReady(handle)
chatStreamRef.current?.notifyStreamReady(handle)
return
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n "terminal-unavailable" --type ts

Repository: stablyai/orca

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -u

echo "== locate files =="
git ls-files | rg 'mobile-native-chat-terminal-stream|mobile/app/h/.*/session/|terminals|lease' || true

echo
echo "== search terminal-unavailable without type filter =="
rg -n "terminal-unavailable|isMobileNativeChatLeaseReady|terminalUnavailable|'terminal'|\"terminal\"" . || true

echo
echo "== file size candidates =="
for f in $(git ls-files | rg 'mobile-native-chat-terminal-stream|session/\[worktreeId\].tsx$|mobile/app/h/.*/session/\[worktreeId\].tsx$' || true); do
  wc -l "$f"
done

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -u

echo "== target file candidate exact =="
f="$(git ls-files | grep -F 'mobile/app/h/[hostId]/session/[worktreeId].tsx' || true)"
echo "$f"
if [ -n "$f" ]; then wc -l "$f"; fi

echo
echo "== focused search exact string =="
rg -n --fixed-strings "terminal-unavailable" "$(git ls-files)" || true

echo
echo "== focused search lease helper =="
rg -n --fixed-strings "isMobileNativeChatLeaseReady" . || true

echo
echo "== focused search mobile native chat stream source/test files =="
git ls-files | rg -n "mobile-native-chat-terminal-stream|nativeChatTerminalStream" || true

Repository: stablyai/orca

Length of output: 1722


🏁 Script executed:

#!/bin/bash
set -u

echo "== session stream helper =="
sed -n '1,120p' mobile/src/session/mobile-native-chat-terminal-stream.ts
echo
sed -n '1,210p' mobile/src/session/mobile-native-chat-terminal-stream.test.ts

echo
echo "== target app context around stream result flow =="
sed -n '1330,1445p' "mobile/app/h/[hostId]/session/[worktreeId].tsx"

echo
echo "== compose lease/state declarations and controls =="
rg -n -C 3 "markNativeChatInputLeaseReady|chatStreamRef|Composer|composer|leaseReady|terminal-unavailable" "mobile/app/h/[hostId]/session/[worktreeId].tsx"

echo
echo "== use-mobile native hook =="
sed -n '1,230p' mobile/src/session/use-mobile-native-chat-terminal-stream.ts

Repository: stablyai/orca

Length of output: 23705


🏁 Script executed:

#!/bin/bash
set -u

echo "== lock/composer dependency expressions =="
rg -n -C 5 "nativeChatInputLeaseReady|nativeChatInputLockReason|composer|Composer|native chat|chat composer|disabled=.nativeChatInputLeaseReady|disabled.*nativeChat" "mobile/app/h/[hostId]/session/[worktreeId].tsx"

echo
echo "== locate input lease hook source =="
git ls-files | rg 'use-mobile-native-chat-input-lease|mobile-native-chat-input-lease|composer|Composer' | rg -E '\.(ts|tsx|js|jsx)$' | head -80

echo
echo "== inspect terminal unavailable occurrences in all tracked files =="
tmp="$(mktemp)"
git ls-files > "$tmp"
rg -n --fixed-strings "terminal-unavailable" --files-with-from-stdin "$tmp" || true
rm -f "$tmp"

Repository: stablyai/orca

Length of output: 3689


🏁 Script executed:

#!/bin/bash
set -u

echo "== input lease hook candidates =="
git ls-files | grep -F 'mobile/src/session' | grep -E '\.(ts|tsx)$' | grep -F 'mobile-native-chat-input-lease' || true
git ls-files | grep -E 'use-mobile-native-chat-input-lease|mobile-native-chat-input-lease' || true

echo
echo "== composer gate file =="
f="$(git ls-files | grep -E 'inputLockReason|keyboardInset|InputLockReason|KeyboardInset' | head -10 || true)"
printf '%s\n' "$f"
if [ -n "$f" ]; then
  IFS=$'\n' read -d '' -ra matches <<< "$f"
  for m in "${matches[@]}"; do
    [ -n "$m" ] && { echo "--- $m"; wc -l "$m"; }
  done
fi

echo
echo "== all terminal-unavailable occurrences =="
tmp="$(mktemp)"
git ls-files > "$tmp"
grep -R --fixed-strings "terminal-unavailable" "$tmp" || true
rm -f "$tmp"

Repository: stablyai/orca

Length of output: 494


🏁 Script executed:

#!/bin/bash
set -u

echo "== use-mobile-native-chat-input-lease source =="
sed -n '1,260p' mobile/src/session/use-mobile-native-chat-input-lease.ts
echo
sed -n '1,220p' mobile/src/session/use-mobile-native-chat-input-lease.test.ts

echo
echo "== composer gate source with all tracked files =="
grep -R --fixed-strings "inputLockReason" . || true
grep -R --fixed-strings "keyboardInset" . || true

echo
echo "== deterministic control-flow probe for covered terminal-unavailable event =="
python3 - <<'PY'
from pathlib import Path
src = Path("mobile/app/h/[hostId]/session/[worktreeId].tsx").read_text()
helper = Path("mobile/src/session/mobile-native-chat-terminal-stream.ts").read_text()
checks = []

checks.append(("terminal-unavailable not present in repository", "terminal-unavailable" not in Path(".").read_text(errors="ignore")))
checks.append(("isMobileNativeChatLeaseReady rejects terminal-unavailable", "'terminal-unavailable'" in helper and "event.type !== 'subscribed'" in helper))

# Extract callback branches in subscribeToTerminal and classify event handling.
# This is a compact, read-only structural summary keyed only by data.type values observed.
for needle in ["terminals", "error", "terminal-unavailable", "subscribed", "resized", "scrollback"]:
    checks.append((f"subscribe callback mentions {needle!r}", f"data.type === '{needle}'" in src or f'"' + needle + '"' in src))

def classify(event_type, covered=True):
    # Mirror the branch ordering from lines 1388-1486:
    if event_type in ("end", "error"):
        return "terminateStream"
    # NativeChatTerminalStream.isMobileNativeChatLeaseReady(covered, data)
    if event_type != "subscribed" or (covered and event_type == "subscribed" and "leaseReady" in {"lease": False} and False):
        pass
    if event_type == "subscribed" and (not covered or True):
        return "markNativeChatInputLeaseReady/notifyStreamReady"
    # covered-terminal early return
    if covered:
        return "terminal-covered-noop"
    if event_type == "resized":
        return "resized-processed"
    if event_type == "scrollback":
        return "scrollback-processed"
    return "no-match-drop"

for et in ["terminal-unavailable", "subscribed", "error", "end", "resized"]:
    checks.append((f"covered {et!r} branch", classify(et)))

for name, value in checks:
    print(f"{name}: {value}")
PY

Repository: stablyai/orca

Length of output: 6442


🌐 Web query:

MobileNativeChatView inputLockReason waiting composer disabled

💡 Result:

The terms MobileNativeChatView, inputLockReason, and waiting composer disabled appear to describe custom implementation logic within a chat interface, likely related to handling state while a message or process is pending [1][2]. There is no standard, framework-agnostic API or component named MobileNativeChatView across major chat SDKs (such as Stream Chat, React Native Gifted Chat, or Chatwoot) that defines these specific property or state names [3][4][5][6]. In development contexts, these terms often arise in projects attempting to manage UI states during asynchronous operations, such as: 1. Input Locking: Developers often implement custom logic to disable the composer or "lock" input when a message is currently being sent, processed by a bot, or streaming a response to prevent duplicate sends or inconsistent states [7][6][8]. 2. State Management: The inputLockReason: 'waiting' pattern suggests a state machine or conditional rendering approach where the UI inspects a reason code (e.g., waiting, processing, streaming) to determine whether the composer should be disabled or visually modified [1][2]. 3. Custom Implementation: If you encounter this in your codebase, it is likely a specific component or context provider created to handle chat stream management [7][2]. You should search your local repository for the definition of MobileNativeChatView to find the corresponding state management logic, which typically involves checking a message status or a connection/process state before setting the disabled attribute on the underlying text input [3][9][2]. If you are attempting to implement this behavior, modern best practices involve queuing messages typed during processing rather than simply locking the input, which improves the user experience by allowing them to continue drafting while the backend finishes the previous request [7][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -u

echo "== MobileNativeChatView overlay =="
sed -n '1,260p' mobile/src/session/MobileNativeChatView.tsx
echo
sed -n '1,220p' mobile/src/session/MobileNativeChatOverlay.tsx

echo
echo "== MobileNativeChatView tests =="
sed -n '1,240p' mobile/src/session/MobileNativeChatView.test.ts

Repository: stablyai/orca

Length of output: 19996


🌐 Web query:

site:github.com stablyai/orca terminal-unavailable MobileNativeChatComposer sendDisabled waiting for terminal

💡 Result:

The error message "terminal-unavailable" and the status "sendDisabled: waiting for terminal" within the MobileNativeChatComposer are part of the terminal and chat lifecycle management in the stablyai/orca codebase [1][2][3][4]. These states generally indicate that the NativeChatComposer has detected a disconnection or a non-ready state in the underlying terminal session (the pseudo-terminal or PTY) and has therefore disabled input to prevent lost messages or inconsistent state [1][5][6][4]. Key technical insights regarding these states: 1. Terminal Lifecycle and Recovery: Orca manages terminal reliability through a daemon-based architecture [7][8]. If a terminal pane's renderer dies or loses IPC connection while the PTY remains alive, it enters a "fossil" state [1]. Automated "self-heal" mechanisms are triggered by specific error patterns—such as "Hello response timed out" or detected stalls—to remount the pane or replace a wedged daemon without closing the user's shell [1][5][9][7][8]. 2. Composer "sendDisabled" Logic: The MobileNativeChatComposer or similar chat UI components rely on a "sendDisabled" contract to gate user input [3][4][10]. This flag is enabled when the backend detects that the terminal is offline, reattaching, or in a "dead-run" state [6][3]. The goal is to ensure that any messages sent by the user are only dispatched when there is an active, healthy PTY transport to receive them [4]. 3. Common Causes for the Waiting State: - Daemon Wedging: If the terminal daemon is "wedged" (it accepts socket connections but fails the protocol handshake), it may repeatedly report "terminal-unavailable" [7][8]. - Network Partitioning/Sleep: Following network loss, sleep, or roaming, the system may enter a 60-second retry grace period where it attempts to reattach to the PTY before surfacing a permanent disconnected state [5][6]. - Initial Session Race: In some cases, native chat components may race the creation of a session's transcript or PTY, causing a temporary "waiting" state while the system confirms the session is ready [2][11]. If you are encountering this persistently, it typically suggests that the terminal's PTY transport has failed to resolve or reattach within the allowed grace period [5][6]. Users can often resolve this by checking for updates (which contain fixes for these recovery paths) or, in the case of a stuck daemon, manually restarting the daemon process if recommended by your specific version's troubleshooting documentation [7][8].

Citations:


Handle PTY-unavailable events instead of waiting forever.

isMobileNativeChatLeaseReady(*) only accepts type === 'subscribed' lease acknowledgements, and the covered-terminal result handler falls through to the terminal-covered noop after lease processing. A PTY stream event such as type: 'terminal-unavailable' therefore leaves inputLockReason === 'waiting', keeping the composer send path disabled with no retry/backoff triggered. Either expose this as a user-visible lock reason/disconnect state or treat it as terminateStream() so useMobileNativeChatTerminalStream reschedules the covered input-lease subscription.

Comment on lines +292 to +303
it('lease-only resubscribe preserves fit hold without becoming query authority', async () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 })
expect(runtime.isMobileTerminalQueryReplyAuthority('pty-1', 'client-a')).toBe(true)
expect(runtime.hasRemoteTerminalViewSubscriber('pty-1')).toBe(true)

runtime.handleMobileUnsubscribe('pty-1', 'client-a')
await runtime.handleMobileLeaseSubscribe('pty-1', 'client-a')

expect(runtime.isMobileTerminalQueryReplyAuthority('pty-1', 'client-a')).toBe(false)
expect(runtime.hasRemoteTerminalViewSubscriber('pty-1')).toBe(false)
})

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the claimed fit-hold preservation.

This test only verifies query/view authority. It never checks that the PTY remains phone-fitted across the lease-only resubscribe, so a regression in fit-hold retention would still pass.

Comment on lines +26331 to +26395
/** Main hook-cache status for a mobile pane, including durable provider transcript identity. */
private getHookAgentStatusForMobileTab(
paneKey: string
): { status: AgentStatusEntry; isFresh: boolean } | null {
const now = Date.now()
let freshest: AgentStatusIpcPayload | null = null
for (const entry of this.getAgentStatusSnapshotFn?.() ?? []) {
if (entry.paneKey !== paneKey) {
continue
}
if (!freshest || entry.receivedAt > freshest.receivedAt) {
freshest = entry
}
}
if (!freshest) {
return null
}
const isFresh =
freshest.providerSessionOnly !== true &&
now - freshest.receivedAt <= AGENT_STATUS_STALE_AFTER_MS
return {
isFresh,
status: {
state: isFresh ? freshest.state : 'done',
prompt: isFresh ? freshest.prompt : '',
updatedAt: freshest.receivedAt,
stateStartedAt: freshest.stateStartedAt,
paneKey,
stateHistory: [],
...(freshest.terminalHandle ? { terminalHandle: freshest.terminalHandle } : {}),
...(freshest.worktreeId ? { worktreeId: freshest.worktreeId } : {}),
...(freshest.connectionId !== undefined ? { connectionId: freshest.connectionId } : {}),
...(freshest.tabId ? { tabId: freshest.tabId } : {}),
...(freshest.agentType ? { agentType: freshest.agentType } : {}),
...(isFresh && freshest.toolName ? { toolName: freshest.toolName } : {}),
...(isFresh && freshest.toolInput ? { toolInput: freshest.toolInput } : {}),
...(isFresh && freshest.interactivePrompt
? { interactivePrompt: freshest.interactivePrompt }
: {}),
...(isFresh && freshest.lastAssistantMessage
? { lastAssistantMessage: freshest.lastAssistantMessage }
: {}),
...(isFresh && freshest.interrupted ? { interrupted: true } : {}),
...(isFresh && freshest.orchestration ? { orchestration: freshest.orchestration } : {}),
...(isFresh && freshest.subagents ? { subagents: freshest.subagents } : {}),
...(freshest.providerSession ? { providerSession: freshest.providerSession } : {}),
...(isFresh && freshest.promptInteractionKey
? { promptInteractionKey: freshest.promptInteractionKey }
: {})
}
}
}

private resolveMobileAgentHookWorktreeId(entry: AgentStatusIpcPayload): string | null {
const tabId = entry.tabId ?? parsePaneKey(entry.paneKey)?.tabId
if (tabId) {
for (const snapshot of this.mobileSessionTabsByWorktree.values()) {
if (snapshot.tabs.some((tab) => tab.type === 'terminal' && tab.parentTabId === tabId)) {
return snapshot.worktree
}
}
}
return entry.worktreeId ?? 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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)orca-runtime\.ts$' || true

echo "== target section =="
sed -n '26300,26420p' src/main/runtime/orca-runtime.ts

echo "== sibling references =="
rg -n "getFreshExplicitAgentStatusForHandle|attachAgentRowsToSummaries|ProviderSessionOnly|providerSessionOnly" src/main/runtime/orca-runtime.ts

echo "== relevant sibling sections =="
sed -n '13970,14030p' src/main/runtime/orca-runtime.ts
sed -n '14730,14790p' src/main/runtime/orca-runtime.ts

echo "== exact text scan for freshest/providerSessionOnly =="
python3 - <<'PY'
from pathlib import Path
p = Path('src/main/runtime/orca-runtime.ts')
s = p.read_text()
for needle in [
    "private getHookAgentStatusForMobileTab",
    "private getFreshExplicitAgentStatusForHandle",
    "attachAgentRowsToSummaries",
    "providerSessionOnly",
]:
    idx = s.find(needle)
    print(f"\n-- {needle} @ {idx} --")
    for start in range(max(0, idx-2000), idx+1200, 1):
        line = s.find("\n", start)
        if line != -1:
            print(f"{((line+1) - idx // 10000)}.{((line+1) - idx) % 10000}")
PY

Repository: stablyai/orca

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path

src = Path("src/main/runtime/orca-runtime.ts").read_text()

for fn_name in [
    "getHookAgentStatusForMobileTab",
    "getFreshExplicitAgentStatusForHandle",
    "attachAgentRowsToSummaries",
]:
    start = src.index(fn_name)
    end = src.find("\n  private ", start + len(fn_name))
    if end == -1:
        end = src.index("\n}", start)
    body = src[start:end]
    lines = body.splitlines()
    print(f"\n== {fn_name} ==")
    for i, line in enumerate(lines[:85], 1):
        if "providerSessionOnly" in line or line.strip().startswith("let freshest") or line.strip().startswith("let bestStatus") or line.strip().startswith("for (const"):
            print(f"{i}: {line}")
    print("body_sample:", "\n".join(lines[:120][:120]))
PY

Repository: stablyai/orca

Length of output: 7984


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact methods containing selector logic =="
grep -n "freshest: AgentStatusIpcPayload\|continue\n\s*if \(entry\.providerSessionOnly === true\|if \(entry\.providerSessionOnly === true\)" src/main/runtime/orca-runtime.ts || true

echo "== line ranges around exact matches =="
while IFS=: read -r line_no rest; do
  start=$(( line_no - 35 ))
  end=$(( line_no + 95 ))
  [ $start -lt 1 ] && start=1
  echo "--- around $rest at line $line_no ---"
  sed -n "${start},${end}p" src/main/runtime/orca-runtime.ts | cat -n | sed -n "1,140p"
done < <(grep -n "freshest: AgentStatusIpcPayload\|getFresh.*AgentStatusForHandle\|attachAgentRowsToSummaries" src/main/runtime/orca-runtime.ts)

echo "== focused programmatic parse =="
python3 - <<'PY'
from pathlib import Path
import re

src = Path("src/main/runtime/orca-runtime.ts").read_text()

def extract_func(name):
    idx = src.index(name)
    after = src.find("\n", idx)
    brace = src.find("{", after)
    depth = 0
    start = src.find("\n", brace)
    for i in range(brace, len(src), 1):
        if src[i] == "{":
            depth += 1
        elif src[i] == "}":
            depth -= 1
            if depth == 0:
                return src[offset := (after if after < i else i) : i+1]
    return None

for name in (
    "private getHookAgentStatusForMobileTab",
    "private getFreshExplicitAgentStatusForHandle",
    "private attachAgentRowsToSummaries",
):
    body = extract_func(name)
    print(f"\n== {name} ==")
    print("contains providerSessionOnly before freshest/best loop:", any(
        re.search(r"providerSessionOnly[\s\S]{0,200}(?:\b(?:freshest|bestStatus|rowSources|bestUpdatedAt)\b)", body)
    ))
    for marker in ["providerSessionOnly", "freshest", "bestStatus"]:
        print(f"{marker}: index", body.find(marker))
    for i, line in enumerate(body.splitlines(), 1):
        if "providerSessionOnly" in line or re.search(r"\b(freshest|bestStatus)\b", line) or "entry.paneKey !== paneKey" in line:
            print(f"{i}: {line}")
PY

Repository: stablyai/orca

Length of output: 313


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== occurrences of providerSessionOnly with adjacent selectors =="
rg -n "providerSessionOnly|let freshest: AgentStatusIpcPayload|let bestStatus|let rowSources|bestUpdatedAt|continue" src/main/runtime/orca-runtime.ts | sed -n '1,220p'

echo "== getHookAgentStatusForMobileTab exact body =="
start=$(grep -n "private getHookAgentStatusForMobileTab(" src/main/runtime/orca-runtime.ts | head -n1 | cut -d: -f1)
sed -n "$((start-5)),$((start+85))p" src/main/runtime/orca-runtime.ts | cat -n

echo "== getFreshExplicitAgentStatusForHandle exact body =="
start=$(grep -n "private getFreshExplicitAgentStatusForHandle(" src/main/runtime/orca-runtime.ts | head -n1 | cut -d: -f1)
sed -n "$((start-5)),$((start+85))p" src/main/runtime/orca-runtime.ts | cat -n

echo "== attachAgentRowsToSummaries exact body =="
start=$(grep -n "private attachAgentRowsToSummaries(" src/main/runtime/or 2>/dev/null || grep -n "private attachAgentRowsToSummaries(" src/main/runtime/orca-runtime.ts | head -n1 | cut -d: -f1)
sed -n "$((start-5)),$((start+95))p" src/main/runtime/orca-runtime.ts | cat -n

Repository: stablyai/orca

Length of output: 18260


Select providerSessionOnly entries only for providerSession, not state freshness.

getHookAgentStatusForMobileTab uses the absolute freshest matching entry for isFresh/state, so a lease-only ping after a newer real status update makes the mobile pane look stale. Split the scan: choose the freshest non-providerSessionOnly entry for status, but still pick the overall freshest entry when merging providerSession.

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.

1 participant