feat: opt-in heartbeat liveness detection #3

Merged
w33ble merged 5 commits from feat/heartbeat into feat/reconnect 2026-09-29 09:09:20 -06:00
Collaborator

Stacked on #2 (feat/reconnect) -- rebase/retarget to main once that merges.

Summary

  • Opt-in heartbeat, off by default: with --heartbeat the bridge sends an MCP ping after --heartbeat-interval (default 30s) with no traffic in either direction; a pong missing past --heartbeat-timeout (default 10s) drops the connection into the normal reconnect path.
  • New flags: --heartbeat (bool, bare flag enables) plus --heartbeat-interval and --heartbeat-timeout durations, all with MCP_BRIDGE_* env fallbacks. Non-positive --heartbeat-interval/--heartbeat-timeout are rejected when --heartbeat is on. Disabled path is structurally identical to before (no ticker, no pings).
  • Ping state is fresh per connection: distinctive mcp-ferryman-ping- IDs, exact-ID pong match swallowed before stdout forwarding (a pong arriving past the deadline trips the timeout instead; other prefixed IDs swallowed, so unsolicited responses never reach the client). Any normal traffic in either direction abandons the pending ping and resets to idle.
  • Activity timestamp (plain int64, single pump goroutine) stamped on remote receives and all remote writes; rejected ping attempts also stamp it so the next attempt waits a full interval instead of firing every tick. Stdin writes report an explicit wrote flag that abandons the pending ping. Ping Write failures reuse the existing rejected-vs-terminal classification.
  • Tests: matcher/expiry/tick/flag-parsing/validation/handleTick-timeout/rejected-ping/abandon/wrote-flag unit tests plus smoke tests (quiet connection stays up with no reconnect; ignored ping triggers reconnect while stdin EOF still exits 0). Testing caught and fixed a nil-heartbeat panic in the disabled path. Build, vet, lint, and tests all green.

Why

  • Reconnect only fires on explicit errors, so a wedged-but-open server (socket alive, no data, no close) hangs client requests forever on any transport. The heartbeat converts that silence into a reconnect.
Stacked on #2 (feat/reconnect) -- rebase/retarget to main once that merges. Summary - Opt-in heartbeat, off by default: with --heartbeat the bridge sends an MCP ping after --heartbeat-interval (default 30s) with no traffic in either direction; a pong missing past --heartbeat-timeout (default 10s) drops the connection into the normal reconnect path. - New flags: --heartbeat (bool, bare flag enables) plus --heartbeat-interval and --heartbeat-timeout durations, all with MCP_BRIDGE_* env fallbacks. Non-positive --heartbeat-interval/--heartbeat-timeout are rejected when --heartbeat is on. Disabled path is structurally identical to before (no ticker, no pings). - Ping state is fresh per connection: distinctive mcp-ferryman-ping-<n> IDs, exact-ID pong match swallowed before stdout forwarding (a pong arriving past the deadline trips the timeout instead; other prefixed IDs swallowed, so unsolicited responses never reach the client). Any normal traffic in either direction abandons the pending ping and resets to idle. - Activity timestamp (plain int64, single pump goroutine) stamped on remote receives and all remote writes; rejected ping attempts also stamp it so the next attempt waits a full interval instead of firing every tick. Stdin writes report an explicit wrote flag that abandons the pending ping. Ping Write failures reuse the existing rejected-vs-terminal classification. - Tests: matcher/expiry/tick/flag-parsing/validation/handleTick-timeout/rejected-ping/abandon/wrote-flag unit tests plus smoke tests (quiet connection stays up with no reconnect; ignored ping triggers reconnect while stdin EOF still exits 0). Testing caught and fixed a nil-heartbeat panic in the disabled path. Build, vet, lint, and tests all green. Why - Reconnect only fires on explicit errors, so a wedged-but-open server (socket alive, no data, no close) hangs client requests forever on any transport. The heartbeat converts that silence into a reconnect.
feat: opt-in heartbeat liveness detection
All checks were successful
CI / Build, vet, test, lint (pull_request) Successful in 57s
32e4ecea26
Disabled by default. With --heartbeat, the bridge pings the
remote after --heartbeat-interval (default 30s) with no traffic
in either direction; a pong missing past --heartbeat-timeout
(default 10s) drops the connection into the normal reconnect
path. Ping responses are matched by exact ID and never forwarded
to the client. All flags have MCP_BRIDGE_* env fallbacks.
w33ble approved these changes 2026-09-29 09:06:47 -06:00
w33ble merged commit c4e9849523 into feat/reconnect 2026-09-29 09:09:20 -06:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
w33ble/mcp-ferryman!3
No description provided.