Skip to content

Project

Troubleshooting

Find your symptom in the quick reference, or jump to the error-code catalog if a tool returned a code in SCREAMING_SNAKE_CASE.

SymptomGo to
CDP won’t connect / 1006Connection
DevTools keeps disconnectingConnection
cdp_store_state empty or errorsStore state
Plugin/skills not detectedPlugin install
Install/update fails (ENOTEMPTY, ENAMETOOLONG)Plugin install
Taps/flows fail on the deviceDevice & flows
A tool returned RN_…/BUSY_…/RUNNER_…Error codes
Action replay failsActions & setup

Run rn_session(action="status") first. For an integrated session, use literal pnpm ios or pnpm android; the package adapter starts or validates Metro on the session’s allocated port. Ambient default-port discovery is diagnostic only and cannot establish authority. See Parallel session authority.

Open the app on the simulator and ensure Hermes is enabled. Check your app.json or metro.config.js for Hermes configuration.

Close React Native DevTools, Flipper, or Chrome DevTools — only one debugger can connect at a time. The 1006 close code means session conflict.

DevTools shows “Disconnected due to opening a second DevTools window”

Section titled “DevTools shows “Disconnected due to opening a second DevTools window””

The bridge auto-reconnects by default and evicts the visual React Native DevTools from the single debugger seat. Set RN_CDP_AUTOCONNECT=0 (or .rn-agent/config.json{ "cdp": { "autoConnect": false } }) to let DevTools hold the seat — the bridge then connects only when a CDP tool runs. Full guide: React Native DevTools coexistence.

This is normal. cdp_reload reconnects the authority-bound target within 15 seconds. If it fails, call cdp_status for passive diagnostics, then cdp_connect to pin the exact signed target again.

Fixed since #264: the bridge entry point is a stdio supervisor that holds no network sockets, so port-based kills (lsof -ti tcp:8081 | xargs kill -9) only take the worker process — the supervisor respawns it automatically and the session keeps its tools. If tools error with “worker is crash-looping”, check the bridge log (LOG_LEVEL=info writes one) and restart the Claude Code session. RN_BRIDGE_SUPERVISOR=0 opts back into the legacy single-process bridge.

Run /rn-dev-agent:setup — it scaffolds the dev-bridge and proposes a getBridge()?.registerStores({ ... }) line in your app entry. The bridge call replaces the older globalThis.__ZUSTAND_STORES__ pattern.

Manual fallback if you want to wire it yourself:

import { getBridge } from './.rn-agent/dev-bridge';
getBridge()?.registerStores({
auth: useAuthStore,
cart: useCartStore,
});

Redux is auto-detected — no setup needed.

cdp_store_state returns empty for React Query

Section titled “cdp_store_state returns empty for React Query”

Use storeType: 'react-query' explicitly. React Query state is read from the QueryClient, which requires the provider to be mounted.

Claude: run /plugin install rn-dev-agent@rn-dev-agent, then /reload-plugins. Local development uses claude --plugin-dir /path/to/rn-dev-agent.

Codex: inspect codex plugin list --json and /mcp verbose. The latter is inventory only, never reconnect. Local development uses /path/to/rn-dev-agent/packages/codex-plugin. If materialization is absent, user-confirm:

Terminal window
codex plugin marketplace upgrade rn-dev-agent
codex plugin add rn-dev-agent@rn-dev-agent --json

Codex 0.145.0 is the live-refresh floor. A change made through the same running app can affect a subsequent turn; external CLI/manual changes and older hosts require exiting and relaunching Codex. Never kill a bridge owned by another Claude/Codex process.

If the plugin is materialized but this task has no rn-dev-agent skills, invoke the package health program by the exact configured cache version:

Terminal window
VERSION="$(codex plugin list --json | jq -er \
'[.[] | select(.pluginId == "rn-dev-agent@rn-dev-agent" and .enabled != false)] | \
if length == 1 then .[0].version else error("expected one enabled rn-dev-agent row") end')"
case "$VERSION" in (*[!0-9A-Za-z._+-]*|'') echo "invalid plugin version" >&2; exit 2;; esac
PLUGIN_ROOT="${CODEX_HOME:-$HOME/.codex}/plugins/cache/rn-dev-agent/rn-dev-agent/$VERSION"
test "$(jq -r '.name + "@" + .version' "$PLUGIN_ROOT/.codex-plugin/plugin.json")" = \
"rn-dev-agent@$VERSION" || exit 2
node "$PLUGIN_ROOT/bin/plugin-health.js" --json

This fails closed on zero/multiple rows or a manifest mismatch and never runs plugin add just to discover a path. If the cache root is absent, the plugin is not materialized: review codex plugin list --json and user-confirm the install sequence instead.

Health reports independent install/materialization/registration/contract/schema axes; unknown task facts are never called stale. It is read-only and does not attach to the app or change plugin/process state.

Update Claude Code to v1.0.33+: npm update -g @anthropic-ai/claude-code

Claude: run /reload-plugins or clear its plugin cache. Codex: a supported same-app mutation should appear on a later turn; otherwise relaunch. A supported Codex install exposes exactly 27 rn-dev-agent skills and no source-command-* skills.

Clear the stale cache first:

Terminal window
rm -rf ~/.claude/plugins/cache/rn-dev-agent

Then retry the update.

Clear stale cache:

Terminal window
rm -rf ~/.claude/plugins/cache/rn-dev-agent ~/.claude/plugins/data/rn-dev-agent-*

Then re-add marketplace and install.

Maestro runner pin is missing or below the floor

Section titled “Maestro runner pin is missing or below the floor”

Run the host setup workflow. It installs and verifies attested maestro-runner 1.1.24 (floor >= 1.1.24) in rn-dev-agent’s versioned pin-cache. Do not add an ambient runner to PATH or install brew Maestro as a substitute; replay accepts only a pinned-ok pin-cache diagnosis.

The crash is native, not JavaScript. Use adb logcat -b crash (Android) or check Xcode console (iOS) for native crash logs.

Use device_list to diagnose connected emulators, then bind the intended serial and app ID with rn_session(action="bind_device", ...). The plugin never turns the first available device or ANDROID_SERIAL into session authority. Without a bound session, maestro_run still needs one exact serial and accepts an explicit deviceId or ANDROID_SERIAL as its runner target only; an ambient or default adb target is never selected.

Run the flow through rn-dev-agent’s replay tools. They use the pin-cache maestro-runner and never fall back to classic Maestro.

UiAutomation not connected when a flow starts on Android

Section titled “UiAutomation not connected when a flow starts on Android”

An MCP reconnect can leave plugin-owned Android instrumentation behind, so maestro-runner fails session creation with failed to create driver: create session: session not created: java.lang.IllegalStateException: UiAutomation not connected. Recovery triggers only when the Maestro subprocess exits nonzero and that structured session-creation record is its entire stderr. maestro_run then force-stops only the two rn-dev-agent runner packages on the exact bound serial and retries the flow once. On a successful or warning response, data.androidUiAutomationRecovery reports retried and retryCount (never more than 1); on a failed or refused response, the same diagnostic is under meta.androidUiAutomationRecovery. Maestro’s own orphaned session self-heals, so no Maestro, Appium, or foreign package is ever killed. A copy of the same text anywhere else — app logcat, console or network output, a partially executed flow’s step output, or stderr mixed with other records — is not the wedge and never retries or releases packages.

Recovery never extends the flow: both the cleanup and the retry run inside the original maestro_run deadline. With no budget left the cleanup is skipped without touching the device; if the cleanup fails or the budget lapses while it runs, the retry is never dispatched and the timeout or release failure is attached to the returned error as its cause. Every one of these paths preserves the original wedge failure, exposes a release warning, and truthfully reports retried: false / retryCount: 0. The diagnostic is present whenever recovery was attempted, including these skipped cases; retried becomes true only when a second Maestro subprocess actually runs.

Pre-flow and recovery release problems are not swallowed. Successful and warning responses expose them as data.androidSlotReleaseWarnings and in the response warning text; failed and refused responses expose them as meta.androidSlotReleaseWarnings. If the same wedge survives the single retry, re-open the device session (device_snapshot action=open) on that serial and retry.

Codes returned by cdp_*/device_*/flow tools, what they mean, and the fix.

CodesMeaning and repair
SESSION_NOT_INITIALIZEDNo fenced session is available. Run setup for the intended worktree instead of selecting ambient state.
SESSION_AUTHORITY_REQUIREDThe requested operation lacks authority. Inspect rn_session(action="status"); when state is blocked, follow recoveryRequirement.nextAction and resolve any startupCleanupBlocked refusal before restarting. Never rebind around a blocked session. For other states, follow the operation’s exact refusal and next action. A headless session that cannot run /mcp uses the packaged recovery commands instead — see recovering a wedged source root.
SESSION_OWNER_LOSTThe supervised session is terminal. Start a new supervisor to create a successor session.
SESSION_CLI_TIMEOUTThe generated build adapter’s bounded prepare-build, ensure-metro, or complete-build session-CLI call did not return within 120 seconds. Inspect rn_session(action="status"); retry only after the named authority step is healthy. The build fails instead of treating truncated output as success.
SESSION_INITIALIZATION_ROLLBACK_FAILEDSession startup failed and could not prove that every partial claim was released. Do not retry against the same resources until rn_session(action="status") shows their ownership clearly.
SESSION_INTEGRATION_PATH_UNSAFE.rn-agent, its integration directory, or a retained path ancestor was symlinked, replaced, or changed during a fenced operation. Restore the intended real directory tree, inspect any preserved recovery journal, and retry.
AUTHORITY_STORE_UNAVAILABLE, AUTHORITY_STORE_BUSYThe SQLite registry is unavailable or busy. Retry after it is healthy; there is no JSON or warning-only fallback.
PROCESS_BIRTH_UNAVAILABLEA required process identity cannot be read conservatively, including at controller, Metro, and runner authority checkpoints. Restore process-birth probing for the installed rn-dev-agent runtime, restart the MCP transport, then retry; there is no PID-only or warning-only fallback.
SOURCE_WORKTREE_MISMATCH, SOURCE_REVISION_NOT_BUNDLED, SOURCE_ROOT_UNAVAILABLEThe session or strict-proof source does not match the running bundle. Return to the intended worktree and load a fresh initial bundle before retrying. The supervisor also refuses this way when it starts a worker: the bound source root is re-proven at spawn time, so a root that was deleted or made unreadable fails every gated tool with SOURCE_ROOT_UNAVAILABLE, and a different repository recreated at that path (or a symlink swung to a sibling worktree) fails with SOURCE_WORKTREE_MISMATCH — the worker is never silently run in the transport’s startup checkout. Restore the bound root, or restart the MCP transport in the worktree you intend to use.
SOURCE_ROOT_DIVERGENCEThe projectRoot declared on bind_device, preview_integration, or apply_integration is not the session’s exact source root — a different git worktree, or a different app package inside the same worktree — so the call refuses instead of mutating the bound tree. The refusal names both paths. Rebind with rn_session(action="bind_source", projectRoot="<declared root>"), which releases the session so its successor mints on that root, then retry; restore package integration first if it is applied. bind_source itself returns this code when the declared root cannot be resolved or belongs to a different repository — a foreign tree is never attached.
NON_GIT_MANIFEST_REQUIRED, NON_GIT_ROOT_MISMATCH, NON_GIT_MANIFEST_OUTSIDE_ROOTRepair RN_DEV_AGENT_DECLARED_ROOT and RN_DEV_AGENT_DECLARED_MANIFESTS, then restart the supervisor. See parallel session authority for the required values, containment rules, and refusal contract.
METRO_PORT_CLAIM_CONFLICT, PORT_OCCUPIED_UNOWNED, METRO_AUTHORITY_MISMATCH, METRO_INSTANCE_CHANGED, METRO_START_CLEANUP_UNPROVENThe allocated port conflicts, the bound Metro identity is disproved or mismatched, or failed startup cleanup could not prove both managed processes and the port stopped. Use the session’s literal pnpm ios/pnpm android integration; never adopt or signal the first listening process.
METRO_ORIGIN_MISMATCHThe actual first bundle or a later live native-origin probe does not match this session’s Metro. Raw device_* control reports this code from its live sibling-Metro scan — that scan is what catches a dev client which fell back to another worktree’s Metro — while unprovable evidence remains originAuthority: "not-proven". Re-point the app at the session Metro and retry. See parallel session authority.
METRO_LAUNCHER_ENVIRONMENT_INVALID, METRO_LAUNCHER_EVIDENCE_UNAVAILABLE, METRO_LAUNCHER_ENFORCEMENT_REFUSED, METRO_LAUNCHER_CHILD_SPAWN_FAILED, METRO_LAUNCHER_PRE_EVIDENCE_FAILED, METRO_LAUNCHER_POLICY_UNAVAILABLEManaged Metro failed before authoritative runtime evidence was available. The bounded diagnostic names the failed stage without exposing credentials or local paths. For a policy failure, restore a real, private .rn-agent/integration directory without symlinks or hard-linked policy files; otherwise repair the named stage, then retry the literal package script.
METRO_LAUNCHER_EXITED, METRO_LISTENER_EXITEDThe authenticated managed Metro launcher or listener exited. Inspect rn_session(action="status") and its bounded metroTerminal.attribution; see parallel session authority for what it can safely contain. Close the native runner if necessary, call stop_metro to complete exact-generation cleanup, repair the attributed cause, and retry the literal package script.
MANAGED_TRANSFORM_CHANNEL_STALLEDAn authenticated transform child exited or did not complete its first exchange before the watchdog expired. Inspect the Metro log for the failing transformer, stop the managed Metro generation, repair the transformer or dependency, and retry the literal package script; strict proof remains unavailable for the violated generation.
DEVICE_CLAIM_CONFLICT, DEVICE_AUTHORITY_MISMATCH, PLATFORM_AUTHORITY_MISMATCH, APP_INSTALL_IDENTITY_CHANGEDThe exact UUID/serial, platform, or app differs, or the installed artifact is no longer the attested session build. Bind the intended device and rebuild or reinstall that generation. A byte-identical reinstall is re-issued automatically on the next gated call, except while a strict proof run is bound; see parallel session authority. bind_device also refuses with DEVICE_AUTHORITY_MISMATCH while an Observe instance you started yourself holds the device axis — run observe action stop, then retry. Only the session-autostarted Observe yields automatically, and the bind_device result then reports observeYielded with the stopped observePort so you can reopen it — an Observe you bring back yourself, with start or restart, counts as caller-started and no longer yields automatically.
DEVICE_AUTHORITY_UNBOUNDAn authority session is present but no device binding is provable, so Observe’s device mirror refuses instead of guessing an ambient device. The session may be unbound, its prior owner unprovable, or the authority store unavailable — the mirror cannot tell them apart. Run rn_session(action="status") and repair the authority axis it names; the mirror clears itself on its own retry loop once the axis is bound.
EXPO_DEVICE_IDENTITY_MISMATCHExpo Android could not uniquely map the bound adb serial to its required model/AVD display name, or that mapping changed immediately before launch. Keep the same exact serial bound, disconnect duplicate model/AVD names, authorize or reconnect that device, and retry literal pnpm android; never pass a display name as session authority or let Expo choose an ambient device.
EXACT_ANDROID_DEVICE_REQUIREDReleasing the plugin-owned Android interaction slot needs one exact serial, and none was available (typically several adb targets attached with no bound session). Nothing on any device was mutated. Bind the intended device with rn_session(action="bind_device", ...), pass deviceId to maestro_run, or set ANDROID_SERIAL, then retry. An ambient or default adb target is never selected.
BUNDLE_HANDSHAKE_UNAVAILABLE, BUNDLE_IDENTITY_MISMATCH, CDP_TARGET_AUTHORITY_MISMATCH, TARGET_CLAIM_CONFLICTThe signed initial-bundle marker or exact CDP target is absent, changed, unresponsive, or owned elsewhere. Exact pinning serially resets an inspector that handshakes but fails its mandatory Runtime.evaluate('1+1') probe, then re-lists only the allocated Metro and retries the same app/device authority. Every exact-target refusal names the stage that actually failed: Metro advertises no debuggable targets, no target carries the proven app identity, the platform association is unproven, no session-matched target is provably on the bound device, or the selection is ambiguous on that device — so a target Metro still lists is never reported as a bare “found 0”, and an expired Android deadline retains that probe-timeout leaf. The session’s own reconnect and relaunch recovery surfaces that connect refusal directly as CDP_TARGET_AUTHORITY_MISMATCH. cdp_connect runs pin_dev_client, the single bundle-authority owner, which normalizes the connect-stage failure’s code to BUNDLE_HANDSHAKE_UNAVAILABLEMETRO_ORIGIN_MISMATCH and BUNDLE_HANDSHAKE_UNAVAILABLE pass through unchanged — while retaining the failing stage in the message after Exact-connect stage:, so the same diagnostic reaches you under the handshake code. Retrying cdp_connect against the same runtime therefore repeats that refusal — relaunch the intended app from the bound Metro through the session’s literal pnpm ios/pnpm android integration first, then re-pin; never choose another target.
RUNNER_OWNERSHIP_MISMATCH, RUNNER_ADOPTION_REQUIRED, RUNNER_CLAIM_CONFLICTNative-runner capability or ownership differs. For RUNNER_OWNERSHIP_MISMATCH, re-open the device with device_snapshot(action="open", …) on the same platform, deviceId, and appId — that restarts and rebinds the interaction runner; rn_session(action="status") only reports state and cannot rebind it. Otherwise rebind the exact runner or complete the explicit handoff/adoption flow. A live runner is never force-stolen; see parallel session authority for the unsupported legacy split-store case. When RUNNER_ADOPTION_REQUIRED appears as startupCleanupBlocked on a blocked contender, the prior owner is already proven dead but its runner could not be proven stopped — run the packaged recovery in recovering a wedged source root.
OBSERVE_AUTHORITY_MISMATCH, OBSERVE_PORT_CLAIM_CONFLICT, PROOF_AUTHORITY_MISMATCHObserve or proof belongs to another fenced generation. Start a session-scoped instance or proof run.
PROJECT_ROOT_UNAVAILABLEThe Observe actions/E2E endpoints could not prove one project root, so they refuse (HTTP 503) rather than serve another checkout’s actions. The root is taken from the bound session’s declared app root, then RN_PROJECT_ROOT, then discovery from the working directory. Bind the intended worktree with rn_session, or set RN_PROJECT_ROOT to that checkout when several declare the same app id. The reason is shown verbatim in the UI.
HANDOFF_NOT_AUTHORIZED, STALE_LEASE_NOT_RECLAIMABLEThe handoff/adoption token is invalid, or the prior owner’s death is not proven. Lease expiry alone never authorizes reclamation. Grouped sessions mint no adoption handles at all: adopt_stale always refuses there, and a proven-dead owner is released by startup cleanup or by the packaged recovery in recovering a wedged source root.
AUTHORITY_LOST_DURING_OPERATION, OPERATION_ALREADY_IN_PROGRESSAuthority changed or another fenced operation is active. Treat any apparent handler success as failed, inspect status, and retry only after the session is stable.

PLATFORM_TARGET_NOT_FOUND / TARGET_PLATFORM_CONFLICT

Section titled “PLATFORM_TARGET_NOT_FOUND / TARGET_PLATFORM_CONFLICT”

cdp_connect refused to connect (or disconnected right after connecting) because the requested platform or bundleId could not be proven for any live Metro target. A target only proves its identity through one internally consistent Metro field — appId, an exact bundle-id description, or the canonical <bundleId> (<device>) title; a defaulted or ambiguous platform guess never satisfies a platform filter. A deviceName that carries no platform token — a custom simulator name such as rn-qa — is proven against the live device inventory (booted simctl simulator names and, only when the session authority binds that exact Android serial, that one device’s adb model — any other initialized authority, including one that binds no device at all, one whose session owner was lost, one whose worker birth or authority store failed to open, or one whose binding cannot be read, fences adb entirely and an ambient adb device is never queried; only a runtime that was never initialized (SESSION_NOT_INITIALIZED) keeps the legacy ambient read); a name that matches both inventories, or neither, stays on the fail-closed bundle inference. Raw device_* tools are not fenced by this rule. The refusal lists the candidate targets with their inference confidence. Run cdp_targets only to diagnose the mismatch, relaunch the authority-bound app, and call cdp_connect again. Omitted values come only from the fenced session; no-filter or explicit ambient-target selection is diagnostic legacy behavior, not authoritative success.

The requested platform or bundle ID conflicts with the active device session’s binding. Inspect passive cdp_status, then request the same platform/bundle or explicitly rebind the session. Cross-platform fallback is refused.

RN_FAST_RUNNER_DOWN / RN_ANDROID_RUNNER_DOWN

Section titled “RN_FAST_RUNNER_DOWN / RN_ANDROID_RUNNER_DOWN”

The in-tree device runner couldn’t start. Check that the simulator/emulator is booted and (Android) the SDK is available. iOS self-builds on first use — a cold build takes several minutes; if it times out, pre-build once with xcodebuild build-for-testing (see Getting Started) and re-open the device session. On iOS a freshly created simulator’s first XCTest bootstrap can overrun the warm ready gate; the bridge absorbs that with exactly one internal retry, so a message ending in (one internal retry included) means two spawn attempts already failed — fix the runner build or the simulator rather than retrying by hand.

The native runner started, but launching the app itself failed (device_snapshot action=open on Android) — reported separately from RN_ANDROID_RUNNER_DOWN so a launcher problem isn’t mistaken for a runner problem. Check that the exact appId is installed on the selected device (adb -s <serial> shell pm list packages). Keyless AVDs are handled automatically: the launcher runs monkey with --pct-syskeys 0, so missing physical system keys no longer abort the launch with exit 251.

A foreign Maestro/XCUITest session (e.g. a standalone maestro-mcp) is driving the same simulator. Wait for it to finish (the guard clears within ~5 s), use CDP reads and device_screenshot meanwhile, or disable the guard with RN_IOS_FOREIGN_GUARD=0.

Your own Maestro flow holds the device (L3 is exclusive). Conditional picker, fill, and dialog fallbacks dynamically enter this plane only when inline Maestro actually dispatches. Reads (cdp_component_tree, cdp_store_state) still work mid-flow; taps are refused on purpose until the flow ends.

A plugin-owned Maestro subprocess ended or timed out, but bounded SIGTERM / SIGKILL cleanup could not positively confirm that its detached process group is absent. The response includes a redacted process-group identity and a manual kill -TERM -<pgid> command. Run that command and retry in the same bridge; the bridge refuses another inline run for the device until absence is observed.

maestro_run/maestro_test_all/cdp_run_action forwarded the active session’s exact device ID (--device/--udid) but maestro-runner’s own report/log did not prove it ran there: the selected device was missing, ambiguous, different from the request, carried only a weak identity, or an iOS WDA build/start line named another device. The replay is refused even when the runner exited 0, because requested metadata is never execution proof — meta.deviceAuthority carries the observed IDs and the exact reason. Re-open the device session on the intended simulator/emulator (device_snapshot action=open), shut down stray devices, and retry; pass deviceId explicitly to maestro_run or maestro_test_all to pin the target.

reason: 'reported-device-weak-identity' is the one case where stray devices are not the cause. Your maestro-runner build’s report writer emitted only a last-resort identifier — a bare model name such as iPhone-16-Pro, or an id field naming the run rather than the device — so nothing was proven either way and the replay fails closed. Shutting down simulators will not clear it; run the current host setup workflow to converge the pin-cache to attested 1.1.24 (floor >= 1.1.24) and retry. If that attested build still emits only a weak identity, the replay remains terminal rather than substituting another engine.

The Android target runs below API 26, which the pinned maestro-runner cannot drive — its bundled UiAutomator2 server APK declares minSdk 26, so the install is rejected with INSTALL_FAILED_OLDER_SDK. When the exact serial is known, maestro_run probes the target’s API level up front (adb shell getprop ro.build.version.sdk) and refuses before the flow starts; a runtime install reject maps to the same code, so this never surfaces as an opaque runner error or a false ownership mismatch. A probe that cannot answer fails open, so probe flakiness never blocks a supported device.

There is no repair for the pinned engine: use the device_* interaction tier instead, which drives API 23+ through rn-android-runner. The few device_* paths that fall back to maestro (dev-client picker and system dialogs) hit the same limit. See engine version pinning.

The bridge and the native runner disagree on the wire protocol and the automatic reinstall didn’t clear it. iOS: delete scripts/rn-fast-runner/build/DerivedData and re-open the device session (cold rebuild). Android: rebuild the runner APKs (./gradlew :app:assembleDebug :app:assembleDebugAndroidTest).

RUNNER_COMMANDS_STALE / UNSUPPORTED_COMMAND

Section titled “RUNNER_COMMANDS_STALE / UNSUPPORTED_COMMAND”

The runner build predates a newer command verb. Re-open the device session (device_snapshot action=open) — the stale artifact is rebuilt automatically (one multi-minute build on iOS). If a cold build still reports missing commands, update the plugin checkout.

The iOS runner didn’t answer within the command budget. On a non-cancellable type/fill main-thread timeout the runner is poisoned and reaped, and device_fill fails closed with a possible mutation outcome. Further commands are refused before dispatch while the poisoned runner is being reaped. Inspect the current field state before deciding whether a new fill is safe, then re-open the device session and use a fresh snapshot — do not re-type blindly.

NO_TEXT_INPUT_TARGET / TEXT_TARGET_LOST / TEXT_TARGET_FOCUS_FAILED / TEXT_ENTRY_UNVERIFIED

Section titled “NO_TEXT_INPUT_TARGET / TEXT_TARGET_LOST / TEXT_TARGET_FOCUS_FAILED / TEXT_ENTRY_UNVERIFIED”

device_fill refuses anything it cannot prove. NO_TEXT_INPUT_TARGET means no single exact editable target could be bound from the fresh snapshot (missing identity, ambiguous testID, target lost, or a non-text-input node). TEXT_TARGET_LOST means the bound owner disappeared during the fill. TEXT_TARGET_FOCUS_FAILED means focus on that exact owner could not be proven. TEXT_ENTRY_UNVERIFIED covers everything else that ends without stable exact read-back: secure or occluded fields, unreadable values, transformed input, and uncertain dispatch.

Every failure carries meta.mutation. none means nothing was typed — refresh the snapshot, resolve one exact input, and retry. observed or possible means the field may already have changed; inspect current state before issuing any new fill. There is no automatic retype, adb input, or Maestro fallback.

A visible software keyboard could not be proven dismissed. iOS automatically tries only a positively identified hide/dismiss button inside the keyboard, then (when CDP is connected) injected Keyboard.dismiss() with a fresh hidden-state post-check. It never swipes through keys, presses Return/Done, or activates an app accessory toolbar. If those safe tiers fail, the guarded app tap is refused. Connect CDP for the JS tier, or dismiss manually and retry from a fresh snapshot/ref.

An iOS Key/Keyboard ref no longer uniquely matches the runner’s retained latest-snapshot record and the operation-local live keyboard. A missing software keyboard, changed layout/identity/frame, ambiguous match, forged metadata, or stale generation produces this mutation-free refusal; it does not enter keyboard-dismiss healing. Capture a fresh snapshot and retry the new ref. Raw coordinates and custom-keyboard Button/Other nodes never qualify as exact keyboard targets. A stale runner missing EXACT_KEYBOARD_TARGET_GUARD must be rebuilt by reopening the exact-device session before dispatch.

A DEVICE_BUSY refusal reports whether the holder is alive and its heartbeat age bounded to 0–90s. It does not expose the holder PID, project path, or app ID, and it does not mutate the holder.

From the holder worktree, run device_snapshot action=close to release it safely.

Alternatively, boot a dedicated simulator (or emulator), bind its exact ID with rn_session action=bind_device, run the normal managed build/install there, then select that exact ID with device_snapshot action=open ... attachOnly=true when the app is already running.

Dead holders self-heal on the next open attempt; live holders self-heal once their heartbeat is stale beyond the existing 90s recovery window.

A healthy live holder is never stolen or changed by this refusal.

DEVICE_CLAIM_CONFLICT is separate: another live worktree session owns the resource and requires explicit handoff or conservatively proven stale adoption; heartbeat or sleep expiry alone does not reclaim it. Do not force-steal or manually install an unrelated snapshot to bypass authority.

Section titled “Native iOS deep-link dialog remains visible”

Prefer device_deeplink; on an open exact iOS session it uses the native runner to accept SpringBoard Open and reports meta.openDialogTapped. If the URL was opened externally or the confirmation remains, call device_accept_system_dialog(label="Open"). A DIALOG_BUTTON_NOT_FOUND result includes availableButtons; retry one exact label and never use coordinate guesses.

An @ref from an old snapshot no longer resolves uniquely. Re-run device_snapshot and use a fresh ref; unique testID/label matches are re-bound automatically, so persistent STALE_REF with a candidates list means the match is ambiguous — disambiguate by testID.

Action replay fails with SELECTOR_NOT_FOUND

Section titled “Action replay fails with SELECTOR_NOT_FOUND”

The plugin attempts to self-repair small UI drift: it looks at the live UI, finds the renamed testID via fuzzy match, patches the action YAML, and retries.

Self-repair refuses if the file has been hand-edited (mtime check), or after 3 repairs in 24h. If repair is refused or fails, the plugin asks before forcing. If the underlying flow has changed structurally (extra step, removed screen), re-record with /rn-dev-agent:test-feature.

See the Actions guide for full details.

Action replay refused with WDA_BOOTSTRAP_FAILED

Section titled “Action replay refused with WDA_BOOTSTRAP_FAILED”

The iOS Maestro driver (WebDriverAgent) failed to bootstrap before the first replay step ran, so the flow was refused and no self-repair was attempted. The verified runner payload remains sealed; rn-dev-agent provisions a separate per-spawn WDA cache and removes it with the runner snapshot after the attempt. Re-run the replay (bootstrap retries itself), check network access, and re-run setup to verify the pin-cache maestro-runner. If the failure detail includes RUNNER_CACHE_UNAVAILABLE, verify that the runner cache parent is writable; the runner was not spawned and any owned snapshot or cache was removed.

Replay refused by the engine floor preflight

Section titled “Replay refused by the engine floor preflight”

Replay requires maestro-runner >= 1.1.24 from rn-dev-agent’s pin-cache and an owned action carrying enginePin: maestro-runner@1.1.24 or newer. Missing, older, checksum-mismatched, unverified, unknown, or unsupported runner states are terminal. Regex text selectors are also refused because the pinned runner does not have a validated implementation for them. Run setup to converge the runner and migrate compatible actions, or rewrite the selector as literal text or an id; no UI mutation occurs before these checks pass.

Re-run /rn-dev-agent:setup. It’s idempotent — already-injected blocks are detected and skipped, so only the missing pieces get applied.

That directory holds runtime state (run history, repair history) — not config. Check that .rn-agent/.gitignore exists and contains state/, recordings/, snapshots/, diag/, index.json. The /setup scaffold ships this file; if it’s missing, regenerate via /setup (idempotent — only the missing pieces are added).