镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(link): drain Child link sockets before a restart stops the tunnel, and count only sockets Codex opened - #8658

Draft
lidge-jun wants to merge 10 commits into
devfrom
feat/link-websocket-child-t6-5
Draft

lidge-jun wants to merge 10 commits into
devfrom
feat/link-websocket-child-t6-5

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

Follow-up to the Child side of the Responses WebSocket over a Remote Link (#8614, #8619, #8620). The final reviews of #8619 and #8620 left items that must be fixed before the Child's release gate opens; this PR fixes them. The gate stays closed, so nothing changes for anyone today.

  • A Child restart no longer pushes Codex onto HTTP. A restart stopped the tunnel supervisor (TERM, up to 5 s, then KILL) before the listener. Meanwhile the tunnel was neither connected nor pending, so Codex's reconnect got 426 and codex-rs used HTTP for the rest of the session. Now LinkTunnelGate has an optional onStopping hook. The supervisor calls it when stop() begins, before it signals the tunnel, and the relay drains: open link sockets close with 1001 on both legs, setups are aborted, and every upgrade from then on, new or still in setup, gets the transient 503 (link_websocket_unavailable, counted as draining). The supervisor also calls it when the link ends (its sidecar is gone or names another link) and the runtime is about to recycle. The hook lives in the supervisor rather than in runtime.ts, so every path that stops the supervisor (restart, recycle, signal) drains first. While draining, an upgrade reaches its 503 even if the Home's last answer turned the advertisement off; a closed Child gate or the Child's opt-out still gives today's 426 with no network I/O. Each serve() builds a new listener and supervisor, so a drained relay is never reused.
  • A local upgrade that failed counts nothing. When server.upgrade returned false, the Child counted closes.stopped, wrote link_ws_close:stopped and counted a complete request in the traffic book for a socket Codex never had. When it threw, the leg stayed in the set with a paused Home socket. Both now go through HomeLeg.abandon(): the Home leg closes with 1001, only the handshake fallback is counted, and the slot is released once the Home connection closes. A socket's traffic flow now begins when Codex's socket opens.
  • A socket Bun upgraded but never opened is bounded. HomeLeg arms the 5 s close bound when it is created and stops itself if attach has not run by then. Scratch runs on the pinned Bun 1.4.4 show that Bun runs open synchronously inside server.upgrade when it returns true, and that server.upgrade returns false (with req.signal aborted) if Codex left before the call. The bound is therefore a guard for runtimes that behave differently.
  • Codex's own end reaches the Home as it happened. Codex's drop (Bun reports 1006 with no reason) now ends the Home connection without a close frame, and a close without a status (1005) becomes an empty close frame; both used to be 1011. A 1006 that Bun reports with its own reason (Bun 1.4.4 reports an oversized message it refused with 1009 as 1006 "Received too big message") still reaches the Home as 1011.
  • Limits of the drain. A restart that takes longer than Codex's retry budget ends that session on HTTP, which is no worse than today's immediate 426.
  • A dropped Codex counts as incomplete. The Child's traffic book now records a socket that ended with 1006 (Codex's drop, or Bun's own close of an oversized message) as incomplete, as the Home counts the same socket; it used to count it complete, and a write still queued toward the Home could be dropped uncounted.
  • A local upgrade that failed after Bun opened Codex's socket (server.upgrade threw after running open) closes both legs as a stop does and counts the socket once; it used to close only the Home leg.
  • Counters and docs. The LinkWebsocketCounters JSDoc and structure/remote-link.md now say that a non-local Origin refused with 403 is not counted, and that fromCodexBytes counts text messages arriving while the Home leg is open. The structure page also explains why the Child's ?transport=1 shape differs from the Home's per-row ws: the Home counts what it did with upgrades per link (refused), while a Child serves one link and its fallbacks are the attempts it sent back to HTTP or told to retry, with reasons only a Child sees. The name is kept.

What keeps it inert: linkWebsocketChildReady() is still false, so the ingress answers today's 426 and the relay never runs. The relay subscribes to the supervisor's stopping hook in every supervised link-mode listener; with the gate closed, the hook flips two flags and walks two empty sets: no I/O, no counting, no diagnostics.

Verification

Tests that failed first (tests/clients/link-ws-relay-edges.test.ts):

  • The supervisor tells its stopping listeners before it signals the tunnel, once. Failed: the hook did not exist.
  • Once the gate is stopping, open sockets close with 1001 and new or in-setup upgrades get the 503. Failed: the relay did not drain, so the open socket stayed up; the test hung on Codex's close and failed by timeout.
  • server.upgrade returning false answers 426, counts no close and no request, and frees the slot. Failed: link_ws_close:stopped was recorded and a request was counted.
  • server.upgrade throwing does the same and leaves no leg behind. Failed: the leg stayed.
  • A socket Bun upgraded but never opened is stopped after the close bound. Failed: no timer was armed.
  • Codex's status-less close reaches the Home as an empty close frame, and its drop ends the Home connection without one. Failed: both reached the Home as 1011.
  • A socket Bun opens clears that bound at once. This one passed before the fix; it is the control for the new bound.
  • A link that ends (sidecar gone) drains too, before the tunnel is signalled. Failed: only stop() notified.
  • A drain still answers 503 after the Home's advertisement turned off. Failed: the ingress's advertisement check came first and answered 426.
  • With the Child gate closed, or the Child opted out, a drain keeps today's 426 and touches no network. A control: it passes on the base too, and pins that the change stays inert, since the drain hook fires even while the gate is closed.

Final-review fixes (failed first unless marked):

  • The drop half of the 1005/1006 test asserts incomplete: 1, the 1005 half incomplete: 0. Failed: the drop counted complete.
  • server.upgrade throwing after Bun opened Codex's socket closes both legs and counts the socket once. Failed: Codex's socket was never closed.
  • The supervisor tests wait on named milestones (tunnel spawned; start-up check done, through a new onInitialized test seam in ClientLinkSupervisorDeps; link ended) instead of microtask counts and a zero timer. Test-only.

Existing tests updated: tests/clients/link-ws-relay.test.ts (the slot-release case awaits the new onReleased seam instead of retrying until a 101; the stop-in-hold case asserts exactly 426; the oversized-message case asserts that the Home gets 1011; two direct HomeLeg cases take a flow factory and mark the socket upgraded, as the relay does) and tests/clients/link-ws-child-status.test.ts (waits on the Child's own release before reading open: 0).

Commands (exit codes) on 03a7cf0 (the T6-5-1 head 8494612 plus one commit per final-review item; it merges cleanly with dev 6f58f62):

  • bun install --frozen-lockfile: exit 0
  • bun run typecheck: exit 0
  • bun run privacy:scan: exit 0
  • bun run structure:check: exit 0
  • bun scripts/test.ts --parallel=2 over tests/clients/link-ws-relay, link-ws-relay-edges, link-ws-child-status, link-ws-fallback, link-ws-transport, link-ws-framer, link-ws-advertisement, link-ws-activity-holds, client-link-tunnel, client-link-runtime, client-machine-listener, client-link-relay, link-relay-bound-transport, link-boundary, tests/e2e-style/link-ws-child-home, tests/e2e-style/link-websocket-release-gate, tests/lab/core-lab-boundary, tests/test-layout, test-layout-tooling, ci-workflows/structure-ssot, ci-workflows/file-size-ratchet: exit 0 (384 pass, 8 skip, 0 fail; the skips are seven release-gate cases that need the pinned codex-rs binary and one macOS-only case)

Review: a gpt-6.1-terra reviewer (xhigh, read-only, its own worktree) reviewed the whole slice. Round 1: no P0-P2; two P3s (the link-ending path did not drain; the ingress's advertisement check could bypass a drain), both accepted and fixed with the tests above, which failed first. Round 2: no P0-P2; one documentation P3 (the structure page's earlier 426 sentence did not name the drain), fixed in cb79c1f. The coordinator's fresh final review of 8494612 found one P2 (a dropped Codex counted complete) and five P3s; each is fixed in its own commit, and a gpt-6.1-terra round on those commits found none. Open P0-P2: 0.

NOT RUN: the full bun run test suite (4-core VM rule: focused files only); macOS/Windows; the release-gate cases in tests/e2e-style/link-websocket-release-gate.test.ts that need the pinned codex-rs binary (OCX_LINK_GATE_CODEX, absent here; they skip).

Checklist

  • Tests in their domain folder with layout entries
  • structure/remote-link.md updated
  • Inert by default (Child gate closed)

…, Codex drop and status-less close, and the counter notes
…ing upgrade reach its 503 whatever the Home last advertised
…ild-t6-5

# Conflicts:
#	scripts/test-layout/layout-clients.json
@coderabbitai

coderabbitai Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Nov 13, 2026
@lidge-jun lidge-jun closed this Nov 13, 2026
@lidge-jun lidge-jun reopened this Nov 13, 2026
@lidge-jun lidge-jun closed this Nov 13, 2026
@lidge-jun lidge-jun reopened this Nov 13, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant