Skip to content

feat(webui): probe the event channel for half-open links and degrade a never-accepted watcher - #270

Merged
Hylouis233 merged 1 commit into
mcode-webui:webuifrom
Hylouis233:fix/webui-watch-liveness
Oct 28, 2026
Merged

Hylouis233 merged 1 commit into
mcode-webui:webuifrom
Hylouis233:fix/webui-watch-liveness

Conversation

@Hylouis233

Copy link
Copy Markdown

What

Closes the two residuals of the Q-区 "error visibility" row that PR #26's review left open (and that the 2026-10-26 satisfaction retest carried forward):

  1. Half-open links were invisible. The channel's health rested on close edges alone; a socket whose network mapping died without a FIN delivers no close event, so a silently dead link kept reading "connected" until the next send.
  2. A never-accepted watcher never degraded. The startup grace let a watcher the server never accepted stay "unknown" for its full window — and a channel that had already gone fatal on it still sat out the grace, so a backend unreachable at load time showed nothing.

How

  • Liveness probe (client-only, no server change): while a socket is open, the channel timestamps every inbound frame — any frame, decoded or not, is proof of life — and asks for one every 10 s using the version operation: the lightest read the surface offers (a synchronous in-memory answer whose validation refuses a body, so the probe frame omits body entirely), carried on a request id derived from this connection's. The answer needs no handler: arriving is the whole point, and it drops through the existing frame.requestId !== requestId filter like anyone else's frame. 25 s of silence is the verdict — the channel forces a reconnect through reconnectNow (backoff deliberately reset, so a server that accepts sockets but never acknowledges watchEvents gets a steady retry rhythm rather than an exponentially spacing one), and the existing close/down rules degrade it while it dials. A hidden page's throttled timers suspend both probe and verdict; the visibilitychange handler already forces a reconnect on return. The numbers are anchored to deadlines the repo already keeps: the server's 15 s socket heartbeat, the 30 s request deadline in wire.ts (which exists for exactly this failure), the 30 s retry ceiling.
  • Grace expiry on fatal: a fatal rejection on an unaccepted watcher ends the startup grace immediately instead of stacking silent seconds on a settled fact; already-accepted watchers are a pure no-op (listeners included).

The remaining gap is deliberate and documented in ConnectionStatus: a fatal channel still borrows the reconnecting copy and offers no retry button, because wiring health.reconnect() into the merge would change the component's semantics — left out rather than half-added.

Verification

  • typecheck:client ✅ typecheck:server ✅
  • New/extended suites green: connection-health.test.ts (20) + webui-channel-health.test.ts (11) — fake-timer-driven probe period/verdict/hidden-exemption/fatal-degradation, and the three aggregation shapes of grace expiry
  • Full suite on this branch: 218/230 files green in the author's Windows environment; the 12 failing files are a byte-identical set to a clean 2e970181 control checkout in the same environment (machine-dialect issues — bsdtar vs GNU tar fixtures, sqlite close timing — none touched by this commit; zero regression)
  • Independent review over the whole diff: approve, 0 blocking; advisory notes cover two comment-anchor precision items and a suggested extra test pin
  • Server path re-verified by the reviewer: judgeWebuiFrame accepts the probe frame shape, validateVersionRequestBody accepts the missing body, dispatch answers synchronously on the same socket, and the probe's reply cannot be mistaken for a watchEvents acceptance

Relation to other work

…a never-accepted watcher

The channel's health rested on close edges alone: a half-open socket (the
network dropped the mapping, neither end was told) delivers no close event,
so a silently dead link kept reading connected until the next send; and the
startup grace let a watcher that was never accepted stay unknown for its
full window even when the channel had already gone fatal on it.

- Liveness probe: while a socket is open, the channel timestamps every
  inbound frame (any frame, decoded or not, is proof of life) and asks for
  one every 10 s - the version operation, the lightest read the surface
  offers: a synchronous in-memory answer whose validation refuses a body,
  carried on a request id derived from this connection's, so the answer
  needs no handler of its own and drops through the existing request-id
  filter. No new frame kind, no server change. 25 s of silence is the
  verdict: the channel forces a reconnect (resetting the backoff
  deliberately, so a server that accepts sockets but never acknowledges
  watchEvents gets a steady retry rhythm rather than an exponentially
  spacing one) and the existing close/down rules degrade it while it
  dials. The numbers are anchored to deadlines the repo already keeps -
  the server's 15 s socket heartbeat, the 30 s request deadline in
  wire.ts, the 30 s retry ceiling. A hidden page's throttled timers
  suspend both probe and verdict; the visibilitychange handler already
  forces a reconnect on return.
- Grace expiry: a fatal rejection on an unaccepted watcher ends the
  startup grace immediately instead of stacking silent seconds on a
  settled fact; already-accepted watchers are a pure no-op.

The remaining gap is deliberate and documented in ConnectionStatus: a
fatal channel still borrows the reconnecting copy and offers no retry
button, because wiring health.reconnect() into the merge would change the
component's semantics - left out rather than half-added.
Copilot AI balanced review requested due to automatic review settings October 27, 2026 17:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@Hylouis233
Hylouis233 merged commit d138c1d into mcode-webui:webui Oct 28, 2026
8 of 9 checks passed
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.

2 participants