Hide native chat UI until terminal lease is ready - #10144
AmethystLiang wants to merge 4 commits into
Conversation
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
📝 WalkthroughWalkthroughThe 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)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
src/main/native-chat/session-file-resolver.test.ts (1)
126-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover the Codex transcript-home fallback.
This test verifies Claude and Grok only, but Lines 33-36 also move Codex’s
~/.codex/sessionsfallback underORCA_NATIVE_CHAT_TRANSCRIPT_HOME_DIR. Add a Codex fixture/assertion withCODEX_HOMEunset.src/main/git/runner.test.ts (1)
195-227: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd 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=falseplus incomingcredential.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 tradeoffSource-text matching makes this test brittle to reformatting.
Asserting on exact substrings/whitespace of the raw
.tsxsource ('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 mockedclient.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
📒 Files selected for processing (29)
mobile/app/h/[hostId]/session/[worktreeId].tsxmobile/scripts/start-emulator-desktop-runtime.mjsmobile/scripts/start-emulator-pairing-runtime.mjsmobile/scripts/start-emulator.mjsmobile/src/session/MobileNativeChatComposer.test.tsmobile/src/session/MobileNativeChatComposer.tsxmobile/src/session/MobileNativeChatView.test.tsmobile/src/session/MobileNativeChatView.tsxmobile/src/session/mobile-native-chat-terminal-stream.test.tsmobile/src/session/mobile-native-chat-terminal-stream.tsmobile/src/session/mobile-native-chat-terminal-subscribe-source.test.tsmobile/src/session/mobile-terminal-diagnostics.test.tsmobile/src/session/mobile-terminal-diagnostics.tsmobile/src/session/use-mobile-native-chat-terminal-stream.test.tsmobile/src/session/use-mobile-native-chat-terminal-stream.tsmobile/src/start-emulator-desktop-runtime.test.tsmobile/src/start-emulator-pairing-runtime.test.tssrc/main/git/runner.test.tssrc/main/index.tssrc/main/native-chat/session-file-resolver.test.tssrc/main/native-chat/session-file-resolver.tssrc/main/native-chat/transcript-watch-liveness.test.tssrc/main/runtime/mobile-subscribe-integration.test.tssrc/main/runtime/orca-runtime.test.tssrc/main/runtime/orca-runtime.tssrc/main/runtime/rpc/methods/terminal.tssrc/main/runtime/rpc/terminal-subscribe-lease-only.test.tssrc/renderer/src/components/terminal-pane/pty-connection.test.tssrc/shared/git-credential-prompt-env.ts
| 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) => { |
There was a problem hiding this comment.
🗄️ 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 || trueRepository: 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 -nRepository: 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).
| (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 | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n "terminal-unavailable" --type tsRepository: 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"
doneRepository: 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" || trueRepository: 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.tsRepository: 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}")
PYRepository: 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:
- 1: UX: explain why the composer is disabled and announce connection state changes djm204/frankenbeast#628
- 2: [Bug]: Message queue / input buffer while agent is processing openclaw/openclaw#52324
- 3: https://getstream.io/chat/docs/sdk/react-native/v8/ui-components/message-input/
- 4: https://getstream.io/chat/docs/sdk/react-native/v5/ui-components/message-input/
- 5: [React Native 0.81+] Android: StatusBar changes automatically | iOS: Cannot type in Composer FaridSafi/react-native-gifted-chat#2661
- 6: Programmatically disable user input while bot is processing chatwoot/chatwoot#12404
- 7: shuowu/pi-tree@8b62936
- 8: AgentsKit-io/agentskit@28181f7
- 9: Add
MessageComposerState.sendEnabledproperty to be able to locally enable/disable sned action GetStream/stream-chat-android#5916
🏁 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.tsRepository: 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:
- 1: fix(terminal): self-heal panes whose renderer dies while the PTY stays alive #8630
- 2:
orca terminal createbackground surface: interactive TUI child (claude) hangs indefinitely before renderer IPC handshake — probabilistic, ~5/7 spawns (macOS 1.4.120) #7442 - 3: IgorGanapolsky/mac-yolo-safeguards@f68b932
- 4: fix(native-chat): scope composer sends to the active turn #8568
- 5: fix(terminal): let pane recovery reach input stuck behind a never-settling connect #8673
- 6: fix(remote-runtime): recover terminals after network loss #9774
- 7: [Bug]: DaemonProtocolError: Hello response timed out #8689
- 8: fix(daemon): replace a permanently wedged daemon instead of preserving it forever (#8689) #8697
- 9: fix(terminal): certify replay wedges only after a quiet window and re-kick recovery on reveal #8691
- 10: https://github.com/IgorGanapolsky/mac-yolo-safeguards/blob/main/hermes-mobile/src/components/ChatInputBar.tsx
- 11: fix(native-chat): retry not-yet-flushed transcripts instead of settling into a permanent error #8418
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.
| 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) | ||
| }) |
There was a problem hiding this comment.
🎯 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.
| /** 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 | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 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}")
PYRepository: 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]))
PYRepository: 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}")
PYRepository: 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 -nRepository: 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.
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
handleMobileLeaseSubscribeRPC path registers input authority without terminal output view subscription or query authorityappendGitConfigEnvnow skips entries already effective in the environment to avoid redundant configurationORCA_NATIVE_CHAT_TRANSCRIPT_HOME_DIRTesting