Skip to content

Bound and remember the remote-realm visibility probe - #6082

Open
habdelra wants to merge 2 commits into
mainfrom
claude/busy-faraday-fn3pi3
Open

Bound and remember the remote-realm visibility probe#6082
habdelra wants to merge 2 commits into
mainfrom
claude/busy-faraday-fn3pi3

Conversation

@habdelra

@habdelra habdelra commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Flaky test fix

Fixes the intermittent failure of Integration | operator-mode | card chooser: cancel button closes the field picker, which fails as
Test took longer than 60000ms; test timed out.

The test is not at fault and its code is untouched here — it times out because
a search it makes waits on a realm-server request that blocks 10 seconds at a
time. The fix is server-side, in the definition lookup; nothing is skipped,
retried, or given a longer timeout.

Not a test-only concern, either: any search filtered on a type from a realm
that has gone offline stalls the same way in production, once per clause, on
every request.

Why it flakes

Placing a module that lives in a realm the serving realm-server does not host
means asking that realm over the network — probeRemoteRealm, reached from
buildLookupContext whenever a lookup's module falls outside every local
realm. That probe's cost was set entirely by the far end: an origin that
accepts no connections spends the platform's full connect timeout (10s under
Node's fetch) before rejecting, none of it is per-module, and nothing was
remembered. A search whose filter names types from an unreachable realm paid
that once per type, and the next search paid the whole bill again.

The host test suite produces exactly that shape. Its virtual realm at
http://test-realm/ is answered in-page by the harness, but a card chooser's
search fans out to every available realm — including the live base realm,
which then tries to reach test-realm for real and cannot. One CI shard's
realm-server log carries 132 such probes, 55 of them paying the full 10s,
arriving in bursts of 10–15 for a single module. The field-picker test opens
the picker, types into it and reopens it, so it absorbs several of those
bursts and runs out of QUnit's 60s budget.

Whether a probe costs 15ms or 10s comes down to what the runner's resolver
does with test-realm — fail fast, or hand back an address that then never
connects. That is the coin flip behind the flake.

The fix

Three bounds now hold the cost flat:

  • A deadline every probe is held to, enforced by the caller.
  • Concurrent probes of one module by one caller share a single request,
    keyed by the requesting user since visibility is answered per caller.
  • A transport failure is remembered against the origin that produced it,
    for the same window an errored definition is cached for
    (ERROR_CACHE_TTL_MS). Keyed by origin because a connection that never
    opens says nothing about the path, and time-boxed so a realm that comes
    back is seen without anything being restarted or cleared. An HTTP answer —
    404 included — is never remembered, since a server that answers at all is
    already cheap to ask.

Why the deadline is enforced on the caller

The probe request carries an AbortSignal as well, so the socket is torn
down wherever the fetch stack honors it, but the bound cannot rest on that
signal alone. The request is rebuilt on its way out by the virtual network's
URL remapping, by the retry wrapper's per-attempt signal merge, and by the
undici dispatcher wrapper, and a platform connect timeout an order of
magnitude longer than the deadline sits underneath all of them. Measured
against a realm-server serving an origin that drops SYNs, a probe carrying a
5s signal still ran to undici's 10s connect timeout. Racing the deadline on
the caller makes the bound hold regardless of which layer the signal
survives.

Diagnostics, so a future timeout is readable

Host tests now record fetches that answered slowly and print them in the
timeout diagnostic. The in-flight snapshot names only what was still
outstanding when the dump ran and the failed-fetch buffer only what rejected,
so a run of requests that each answered in seconds — the signature of this
failure — left no trace in either. This is deliberately kept even though the
fix above looks sufficient: if the flake recurs for a different reason, the
next failure names the endpoint and the cost in the shard log.

Not in scope

No caller of lookupDefinition changes, and the probe's contract is
unchanged: an unreachable or non-answering realm still resolves to "cannot
place this module", which surfaces as FilterRefersToNonexistentTypeError
exactly as before. Only the latency and the number of network round trips
change.

Test plan

packages/realm-server/tests/definition-lookup-test.ts — a new hermetic
remote realm visibility probe module, four tests, one per property:

  • a probe whose remote never answers and ignores the abort signal is
    abandoned on the caller's deadline. The fake remote deliberately ignores
    the signal, because a handler that resolved on abort would pass on the
    strength of the signal alone and say nothing about the caller's bound —
    with the caller-side deadline removed and the signal still attached, this
    test fails with Test took longer than 60000ms; test timed out., the same
    signature as the flake itself
  • three concurrent lookups of one module probe the remote once
  • an origin whose probe failed at the transport is not probed again while the
    record stands, across different modules under it
  • an origin that answers with a status is probed again on the next lookup

All 41 tests in definition-lookup-test.ts pass.

Verified end to end against a local stack with test-realm resolving to an
address that drops SYNs, reproducing the CI runner's
ConnectTimeoutError … timeout: 10000ms. Running the flaky test against that
stack:

realm-server probes 10s connect timeouts test duration
before 24 24 63,341 ms — over budget, the failure
after 2 0 (2 deadline hits at 5,001 ms) 24,356 ms

🤖 Generated with Claude Code

https://claude.ai/code/session_01TJp8v1CnpxAUrbtqcjKQYv

Placing a module that lives in a realm the serving realm-server does not
host means asking that realm over the network. The probe was unbounded
and its outcome was not remembered, so an origin that accepts no
connections cost the platform's full connect timeout - 10s under Node's
fetch - once per lookup, and again on every search that followed.

The host suite produces that shape constantly. Its virtual realm at
`http://test-realm/` is answered in-page, but a card chooser's search
fans out to the live base realm, which then tries to reach `test-realm`
for real. One CI shard's realm-server log carries 132 such probes, 55 of
them paying the whole 10s, arriving in bursts of 10-15 for a single
module - with `Integration | operator-mode | card chooser: cancel button
closes the field picker` exhausting QUnit's 60s budget on top of them.

Three bounds now hold that cost flat: every probe is bounded by a
deadline the caller enforces, concurrent probes of one module by one
caller share a request, and a transport failure is remembered against
the origin that produced it for the same window an errored definition is
cached for. An HTTP answer is never remembered, so a 404 still costs a
probe next time.

The deadline is raced on the caller rather than left to the request's
abort signal. The signal is still passed, so the socket is torn down
wherever the fetch stack honors it, but it cannot be relied on for the
bound: the request is rebuilt by the virtual network's URL remapping, the
retry wrapper and the undici dispatcher in turn, and measured against a
realm-server serving a black-holed origin the probe ran to undici's 10s
connect timeout with a 5s signal attached. Racing on the caller makes the
bound hold regardless of which layer the signal survives.

Reproduced against a local stack with `test-realm` resolving to an
address that drops SYNs, which is what the CI runner's resolver
effectively did. The card-chooser test spent 63.3s over 24 probes, each
hitting undici's 10s connect timeout; with the origin remembered and the
deadline enforced it spends 24.4s over 2 probes of 5.0s each.

Also record per-test fetches that answered slowly and print them in the
host timeout diagnostic. The in-flight snapshot names only what was still
outstanding and the failed-fetch buffer only what rejected, so a run of
requests that each answered in seconds - the signature here - left no
trace in either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJp8v1CnpxAUrbtqcjKQYv
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T20:04:54.222362Z fe3a06e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe3a06e57f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/runtime-common/definition-lookup.ts
Comment thread packages/runtime-common/definition-lookup.ts Outdated
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   2h 35m 31s ⏱️
4 734 tests 4 720 ✅ 14 💤 0 ❌
4 749 runs  4 735 ✅ 14 💤 0 ❌

Results for commit e3f09af.

Realm Server Test Results

    1 files    210 suites   1h 10m 56s ⏱️
2 771 tests 2 771 ✅ 0 💤 0 ❌
2 810 runs  2 810 ✅ 0 💤 0 ❌

Results for commit e3f09af.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Four moderate findings remain involving timeout retries, cache growth, diagnostic accuracy, and cross-user coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR bounds and coalesces remote-realm visibility probes, caches transport failures, and improves timeout diagnostics.

Changes:

  • Adds probe deadlines, per-user coalescing, and origin failure caching.
  • Adds hermetic remote-probe regression tests.
  • Records slow fetches in host timeout diagnostics.
File summaries
File Summary
packages/runtime-common/definition-lookup.ts Implements probe deadlines, coalescing, and failure caching; review findings concern retry handling, cache growth, and cross-user coverage.
packages/realm-server/tests/definition-lookup-test.ts Adds remote-probe timeout, coalescing, caching, and status-response tests.
packages/host/tests/helpers/setup.ts Adds slow-fetch timeout diagnostics; failed requests may be mislabeled as completed fetches.
Review details

Suppressed comments (2)

packages/runtime-common/definition-lookup.ts:1480

  • [Claude Code 🤖] AbortSignal.timeout() rejects fetches with a TimeoutError, but VirtualNetwork.withRetries treats only AbortError as caller cancellation (packages/runtime-common/virtual-network.ts:1043-1049). For retryable probe URLs, this timeout therefore enters the retry loop after withDeadline has returned, issuing background attempts and defeating the transport bound. Make the deadline use an AbortError-compatible controller or teach the retry layer to recognize this caller signal, and add a retryable-URL regression test.
          signal: AbortSignal.timeout(REALM_PROBE_TIMEOUT_MS),

packages/runtime-common/definition-lookup.ts:1445

  • [Claude Code 🤖] The probe result determines cacheScope and cacheUserId, so the user dimension in this key is security-sensitive. The added tests cover coalescing for one caller only; add a concurrent lookup case with two different owners and assert separate HEAD requests and headers, otherwise a regression here could share one user's private/public visibility result with another user.
    // The requesting user is part of the key: visibility is answered per
    // caller, so two users probing one module must not share a result.
    let key = `${moduleURL}|${probeAuthKey(headers)}`;
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/host/tests/helpers/setup.ts Outdated
Comment thread packages/runtime-common/definition-lookup.ts Outdated
Five findings from the automated reviewers, all in the probe added here.

`probeAuthKey` read the request headers with `Headers.entries()`, which is
only typed under a `dom.iterable` lib. runtime-common is type-checked by
packages that enable no such lib, so billing, ai-bot and bot-runner failed
`lint:types` on a file none of them changed. Reads the headers with `forEach`
instead, which every lib version carries.

A probe that is answered now clears its origin's unreachable record. The
record is written on transport failure and read before any probe is made, so
the only probe that can reach an already-recorded origin is one that was
already in flight when the record was written — and on that path an origin
demonstrably answering was being reported as unable to place its types until
the window lapsed.

The unreachable record is swept and capped on insertion. A filter carries
caller-supplied module URLs, and a record was only retired when its own
origin was probed again, which for a one-off origin never happens; the map
could grow for the life of the process.

The deadline aborts with a reason named `AbortError` rather than the
`TimeoutError` an `AbortSignal.timeout` raises. The retry wrapper classifies
by name and treats only a caller's abort as final, so a `TimeoutError` was
retried — leaving attempts running behind a deadline that had already been
reported.

The host timeout diagnostic counts only fetches that answered. Recording from
`finally` also counted rejected ones, which `recentFailedFetches` already
names, so failures were double-reported and inflated a total the dump
presents as the cost of work that succeeded.

Three tests, one per behavioral finding: a deadline on a retryable origin is
not retried, an origin answering one probe clears a record another probe
left, and two owners probing one module each get their own request carrying
their own assumed user. The retryable-origin test rejects its request with the
abort reason the way a real fetch does — a handler that ignores the signal
leaves the request pending, the retry loop only ever sees rejections, and the
test then passes whatever the reason is named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJp8v1CnpxAUrbtqcjKQYv
@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown

[Claude Code 🤖]

The Copilot review listed two further findings as suppressed comments, so they have no threads to answer on. Both are real and both are fixed in e3f09af7.

AbortSignal.timeout raises a TimeoutError, which the retry wrapper does not treat as caller cancellation. Accurate, and the sharpest finding in the review. withRetries classifies by error name and short-circuits only on AbortError; everything else enters the retry ladder. So on a retryable origin the deadline was retried, leaving attempts running behind a bound that had already been reported to the caller — the opposite of a bound. The deadline now aborts with a DOMException named AbortError, and the name has a comment saying why it is load-bearing.

The regression test you asked for is a deadline is not mistaken for a retryable failure on a retryable origin, on a localhost URL so it is genuinely retryable in the suite. Worth noting what it took to make it real: my first version's fake remote ignored the abort signal, so the request stayed pending, the retry loop — which only ever sees rejections — was never reached, and the test passed with the TimeoutError still in place. It now rejects with the abort reason the way a real fetch does, and fails with expected: 1 attempt when the reason is renamed back.

The user dimension of the coalescing key is security-sensitive and only covered for one caller. Fair. Added two owners probing one module do not share a visibility result: two lookups scoped to realms with different owners probe the same module concurrently, and the test asserts two separate requests carrying the two distinct X-Boxel-Assume-User values — so a regression that widened the key would show up as one shared probe rather than as a silently shared visibility answer.

Verification. 44/44 in definition-lookup-test.ts. lint:types now passes for billing, ai-bot and bot-runner, which were the three Lint failures — caused by Headers.entries() in this file needing a dom.iterable lib those packages do not enable.


Generated by Claude Code

@habdelra
habdelra requested a review from a team September 10, 2026 20:40
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.

3 participants