Repository navigation
[Feature]: Add durable Remote Link failure diagnostics and opt-in tunnel debug logging #7297
Description
Area
Multiple areas
What are you trying to accomplish?
Diagnose Remote Link failures after they have recovered. When a Child returns 503 {"error":"link tunnel unavailable"}, operators need durable diagnostics explaining the relay decision, SSH lifecycle and recovery attempts, plus an opt-in way to collect more detail for the next occurrence.
This proposal is based exclusively on the public main source at 6936d98 (2.88.0). It contains no private installation details or logs.
What prevents this today?
The public implementation has several observability gaps:
src/client/link-relay.ts:369-405: missing readiness and multiple fetch failures collapse into the same 503; the caught transport error and gate failure reason are not logged.src/client/link-tunnel.ts:748-756: SSH stderr is classified internally, but the exit code, classification and useful sanitized diagnostic are not emitted as an event.src/client/runtime.ts:289-293: the optional supervisorwarncallback is not supplied, so its warning sites are silent in this runtime wiring.src/link/ssh-argv.ts:86: SSH verbosity is fixed atLogLevel=ERROR.src/cli/debug.ts: the debug scopes cover provider, usage, injection and Claude capture, not Remote Link. A client-role listener cannot enable serving-proxy debug through its local listener.
An instantaneous status cannot explain a failure that has already recovered. The generic 503 cannot distinguish a failed gate/ownership check, reconnect hold expiry, connection refusal/reset, header timeout or caller abort.
What should OpenCodex do?
- Persist low-volume structured events for tunnel transitions, process spawn/exit, failure classification, scheduled retries and successful recovery, for both Child- and Home-initiated links.
- Record a correlated diagnostic for every locally generated tunnel-unavailable 503: timestamp, opaque request/link identifiers, relay phase, tunnel state, stable reason code, safe transport error code, hold duration and next retry time where applicable. Preserve the existing wire response if needed.
- Offer a documented opt-in link debug scope or equivalent setting usable on the Child itself. Expose effective settings, log location, bounded retention/rotation, and a way to turn capture off. Include more detailed readiness/ownership/probe results and safely bounded SSH diagnostics when enabled.
- Keep credentials, authorization headers, prompts/request bodies and account data out of these logs. Sanitize and bound SSH text; avoid event flooding during long outages.
- Verify observable diagnostics for refusal, reset, hold expiry, SSH exits and recovery, including redaction and default-disabled verbose capture.
Example usage or interface
Illustrative proposed interface, not an existing command:
ocx debug link on
ocx debug link status
ocx debug link logs -f
ocx debug link offIllustrative synthetic events, not production logs or a required schema/order:
link.relay_unavailable requestId=<opaque> linkId=<opaque> phase=connect reason=connection_refused tunnelState=reconnecting errorCode=ECONNREFUSED holdMs=15000
link.tunnel_exit linkId=<opaque> exitCode=255 failureClass=network
link.retry_scheduled linkId=<opaque> delayMs=60000
link.recovered linkId=<opaque>
Basic diagnostics should explain a rejected turn even with verbose capture disabled; verbose capture adds bounded detail.
Alternatives or workarounds
Inspect service output and current link status, or reproduce SSH manually with verbose output. These approaches do not necessarily correlate an earlier failed request with the supervised SSH process and its environment. The provider debug scope does not capture the relay's tunnel readiness decision.
Additional context
Related public recovery work: #6899 and #6915. This proposal concerns observability rather than changing retry policy. The Home-initiated recovery fix and the 250 ms post-exit output-drain fix make durable diagnostics useful for distinguishing recovery scenarios without assuming a historical cause.
Public source: https://github.com/lidge-jun/opencodex/tree/6936d9885d8a9bda9301737affcbaf1a4ff00e17
Checks
- I searched existing issues and documentation.
- This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
- I removed secrets and personal data.
Activity
arikon commentedon Oct 18, 2026
Additional results from the macOS Child after upgrading to 2.88.0. The link is Child-initiated (
ssh -L); Home remains on 2.80.0. Personal identifiers, PIDs, addresses and raw headers are omitted.Observations
lsofcommand all found the expected SSH PID in approximately 0.19–0.20 seconds. These checks cannot establish its outcome during the earlier failure./v1/modelsrequests returned 200; an intentionally invalid empty/v1/responsesrequest reached validation and returned 400 without making a model call. Subsequent real Codex requests also received 200. No runtime, service or SSH restart was performed during diagnosis.Source correlation and uncertainty
At main revision
6936d9885d8a9bda9301737affcbaf1a4ff00e17:port-reclaim.ts:363setsOWNER_LOOKUP_TIMEOUT_MS = 2_000.port-reclaim.ts:448–466runslsofon macOS and converts lookup errors or deadline expiry intofalse.link-tunnel.ts:623–652requires fresh listener ownership proof forconnected().link-relay.ts:369–382returns the genericlink tunnel unavailable503 if proof fails and the supervisor is not pending.An intermittent local ownership-lookup timeout is the leading hypothesis, not an established root cause. The repeated approximately two-second signature fits this path while the SSH process stays alive. OpenCodex does not log the failed lookup's duration, exit/output, error or timeout outcome, so the historical result and why it might have been delayed remain unknown.
Other configured waits are weaker matches for this timing: reconnect hold 15 seconds, relay authentication 5 seconds, response headers 300 seconds; the 64-waiter cutoff is immediate.
Please include a correlated ownership-check diagnostic distinguishing match, mismatch, lookup error and deadline expiry, with elapsed time and supervisor state. A stable failure reason such as
listener_owner_lookup_timeoutwould let operators confirm or reject this hypothesis. This does not propose bypassing ownership validation or assume that increasing its deadline is the correct fix.Ingwannu commentedon Oct 18, 2026
The timing is useful evidence, but it still does not identify the historical failed lookup. I checked current dev
8640c2f30edb:port-reclaim.tsretains the 2,000 ms owner-lookup limit and the tunnel admission path consumes a boolean ownership result. A successful later lsof sample cannot prove the failed call matched or timed out.A focused first PR can preserve that admission result while adding a bounded typed diagnostic: match/mismatch/lookup-error/deadline, elapsed time, check generation and supervisor state, correlated to the relay refusal. Cover injected timeout, spawn failure, mismatched PID, stale completion and success; diagnostics must not change retry/admission policy or let stale proof satisfy a new request. Default output should be metadata only, deduplicated/rate-limited and bounded on disk; opt-in SSH verbosity needs a separate bounded/redacted sink, since raw stderr/argv can contain identities and destinations. Please keep deadline tuning and debug-mode UX out of the first evidence slice. No live tunnel restart or bypass is required to test this.
arikon commentedon Oct 19, 2026
Follow-up: the generic 503 has appeared with both approximately two-second and five-second timing signatures. Timing alone cannot establish the historical internal failure category or why an operation was delayed. The earlier ownership-lookup hypothesis remains unconfirmed.
The new result below is a controlled synthetic reproduction, independent of production infrastructure: the public five-second relay-auth deadline can produce the exact same generic 503 before any data request is dispatched. No raw production logs, incident statistics, deployment configuration, identifiers or credentials are included.
Controlled reproduction of the five-second path
Synthetic test against main revision
6936d9885d8a9bda9301737affcbaf1a4ff00e17(2.88.0). With the checkout's dependencies installed, save this asrepro-link-auth-timeout.tsin the repository root and runbun repro-link-auth-timeout.ts(approximately ten seconds total).The fake peer accepts the authentication request but deliberately never replies. It binds only a dynamically allocated loopback port and uses a synthetic key/identity. The injected positive gate isolates the real connection-bound transport from OS listener ownership checks;
fetchImplis not replaced. It does not modify a running tunnel or use production credentials.Observed output (the counters are cumulative across the two sequential probes):
{"probe":"transport","seconds":5.006377042,"name":"AbortError","message":"The operation was aborted","code":"ABORT_ERR","causeName":"TimeoutError","authRequests":1,"dataRequests":0} {"probe":"relay","seconds":5.004834625,"status":503,"body":{"error":"link tunnel unavailable"},"retryAfter":"1","authRequests":2,"dataRequests":0}Thus the generic failure is reproducible after an accepted loopback connection, while the tunnel gate reports connected, before any data request reaches the peer. This proves the code path, not that the historical five-second failures necessarily took it.
Source correlation / diagnostic scope
At the same pinned revision:
relay-auth.ts:6sets the authentication deadline to 5,000 ms.link-relay-transport.ts:161–183applies it to the auth probe and rethrows the resulting transport error. Deadline expiry here producesAbortError/ABORT_ERRwithTimeoutErroras its cause, rather thanLinkRelayAuthenticationError.link-relay.ts:392–405therefore falls through to the genericlink tunnel unavailableresponse, with no underlying-error diagnostic. The existing narrow refused-connection retry does not apply to this abort.For the requested bounded diagnostics: correlated refusal metadata should distinguish ownership lookup outcomes from relay-auth deadline expiry and other transport failures. A dedicated injected auth-timeout regression test should assert that no data request was sent and that diagnostic classification preserves the existing 503/admission/retry behavior. This is not a proposal to increase deadlines, bypass admission, or add automatic replay.
github-actions commentedon Oct 19, 2026
Automated translation bookkeeping — detected language: English.