2435131573
handleFrame guarded JSON.parse but then read `frame.id` on whatever came back, so a stdout line of `null`/`42`/`"str"` threw a TypeError out of the readline handler — an uncaughtException in the Ink process, where main's TUI had caught and logged it. Non-object frames now return null (the owner logs a protocol error, as for non-JSON). The shared heartbeat counted ANY inbound frame as liveness, silently dropping the TUI's original contract (fail on an unanswered gateway.ping): a backend whose request loop is wedged but still streams deltas never tripped the deadline. `heartbeatLiveness` now selects the contract: 'response' (default, TUI) — only a pong or a response to our own request resets the deadline; 'any-inbound' — the desktop/web WebSocket client's original behaviour, which JsonRpcGatewayClient passes explicitly so that surface is unchanged. The dead 'error' branch comment in connect()'s onClose is corrected to describe the onSocketClose-intercept case it actually serves. Tests: it.each over 'null'/'42'/'"str"'/'true' asserts no throw and null; 'response' mode: pongs and request responses keep it alive, streaming deltas with unanswered pings fire onHeartbeatFailure. Sabotage (remove the object check + count any inbound): 5 tests fail with the original TypeError.