Skip to content

fix(middleware): re-bind the React outbound channel when a session reconnects - #477

Merged
V3RON merged 1 commit into
mainfrom
fix/react-outbound-rebind
Sep 1, 2026
Merged

V3RON merged 1 commit into
mainfrom
fix/react-outbound-rebind

Conversation

@V3RON

@V3RON V3RON commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Description

Re-binds the React agent domain to the device when an Agent session reconnects, so React DevTools tools that have to send a message to the app keep working after the app restarts.

createReactDomainService called store.registerDevice(...) exactly once, at construction — that call is what stores the outbound sendMessage. On socket close, session.ts calls service.onDisconnected(), which ran store.unregisterDevice(...) and deleted the per-device state, sendMessage included. The session then recovers on its own, but LocalAgentToolService has no onConnected hook and nothing re-ran registerDevice. The state was recreated lazily by the next inbound message, with sendMessage === undefined.

onDisconnected now clears the stale tree, inspections and profiling state as before, then immediately re-binds the outbound channel.

Related Issue

Closes #476

Context

This is what produced the exact inbound/outbound split in the issue. Inbound survived because ingestReactDevToolsMessage recreates the state on its own, so getTree/getChildren/searchNodes kept returning a correct, freshly ingested tree. Outbound was dead because the bridge is created with sendMessage: state.sendMessage, so send() became a silent no-op:

  • getProps/getComponent check !state.sendMessage explicitly and failed loudly with the reported unavailable-channel error.
  • Profiling does not check. startProfiling flipped the local store to started and dropped the event, reporting {ok: true}; stopProfiling set the phase to processing and dropped the event, so no profilingData ever came back and isProcessingData stayed true indefinitely. waitForProfilingData bounds its own wait, but nothing ever cleared the phase.

session create reported connected because the session genuinely had reconnected — only the React domain's binding was silently dropped.

Re-binding in onDisconnected is the smallest fix that holds: deps.sendReactDevToolsMessage closes over the session's sendDomainMessage, not over one socket generation, so it stays valid across recovery. Adding an onConnected hook to the service contract and threading it through the session's recovery path would work too, but touches every local domain service for no additional benefit here.

Two hardening items from the issue are deliberately left out to keep this focused on the root cause, and are worth separate changes: giving startProfiling/stopProfiling the same !state.sendMessage guard getProps already has, and having the profiling store leave the processing phase on a bounded timeout rather than parking on it.

Testing

Automated:

  • Added react domain service outbound channel > keeps sending to the device after the app reconnects to packages/middleware/src/__tests__/agent-local-domains.test.ts. It ingests real operations payloads, calls onDisconnected(), ingests a fresh tree, and asserts getProps reaches the device and startProfiling is actually sent. Confirmed it fails against the unfixed code with expected [Function] to throw error matching /No props snapshot available/ but got 'React DevTools outbound channel is un…'.
  • pnpm checks:affected — 107 tasks successful.
  • pnpm test:affected — 69 tasks successful.

Manual, on the running iPhone 17 Pro simulator with the playground:

  • Reproduced first against unpatched middleware: after simctl terminate + simctl launch, session create returned status: "connected", getTree returned the new tree (roots renumbered 2441), and getProps on a node from that new tree failed with the outbound-channel error.
  • Rebuilt the middleware with the fix, restarted Metro, and repeated the same relaunch cycle. getTree returns the fresh tree and getProps on node 3 now returns real props.
  • Full profiling cycle after the relaunch, driving the app with agent-device between start and stop: stopProfiling returned {"session":{"roots":[1],"totalCommits":4,"totalRenderDurationMs":42.075},...} — real commits, no partial flag, no isProcessingData hang.

@V3RON
V3RON merged commit f74f1be into main Sep 1, 2026
4 checks passed
@V3RON
V3RON deleted the fix/react-outbound-rebind branch September 1, 2026 05:57
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.

Agent session survives an app relaunch in a half-dead state: reports connected, outbound channel silently gone

1 participant