Repository navigation
feat(webui): probe the event channel for half-open links and degrade a never-accepted watcher - #270
Merged
Hylouis233 merged 1 commit intoOct 28, 2026
Conversation
…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.
There was a problem hiding this comment.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
closeedges 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.How
versionoperation: the lightest read the surface offers (a synchronous in-memory answer whose validation refuses a body, so the probe frame omitsbodyentirely), 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 existingframe.requestId !== requestIdfilter like anyone else's frame. 25 s of silence is the verdict — the channel forces a reconnect throughreconnectNow(backoff deliberately reset, so a server that accepts sockets but never acknowledgeswatchEventsgets 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; thevisibilitychangehandler 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 inwire.ts(which exists for exactly this failure), the 30 s retry ceiling.The remaining gap is deliberate and documented in
ConnectionStatus: a fatal channel still borrows the reconnecting copy and offers no retry button, because wiringhealth.reconnect()into the merge would change the component's semantics — left out rather than half-added.Verification
typecheck:client✅typecheck:server✅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 expiry2e970181control checkout in the same environment (machine-dialect issues — bsdtar vs GNU tar fixtures, sqlite close timing — none touched by this commit; zero regression)judgeWebuiFrameaccepts the probe frame shape,validateVersionRequestBodyaccepts the missing body, dispatch answers synchronously on the same socket, and the probe's reply cannot be mistaken for awatchEventsacceptanceRelation to other work