Skip to content

fix(cli): catalog loads registry items a few at a time so a slow link no longer times them out - #5829

Merged
miguel-heygen merged 3 commits into
mainfrom
scrubfix/catalogfix
Oct 18, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
scrubfix/catalogfix

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What changes

hyperframes catalog --query <q> --json no longer floods stderr with TimeoutError: The operation was aborted due to timeout lines, and no longer quietly searches only part of the catalog, when the registry link is slow. Item manifests now load 32 at a time instead of all ~450 at once. If one does time out, the rest are served from cache instead of waiting on the network. Failures print one summary line.

Why it happened

The catalog search needs each item's registry-item.json; registry.json carries only names and types. loadAllItems started every fetch together through Promise.allSettled. Each fetch has its own 10 s AbortSignal.timeout, and all those clocks started at the same moment while ~450 responses shared one link.

On a link that can't move them all within 10 s, every response still queued at that point timed out together. Each one printed its own skipped item "<name>": TimeoutError ... line, and the search ran over whatever had arrived. Item manifests are cached for 24 hours, so this hits the first run of the day, a fresh machine, or a sandbox with an empty home.

The fix

  • Bounded fetching: loadAllItems runs at most 32 fetches at a time through the existing runWithConcurrencyLimit, moved from commands/lambda/render-batch.ts to utils/concurrency.ts so the registry code doesn't import the Lambda module. Each request's timeout now covers only its own transfer.
  • Dead network stays fast: after a fetch fails with TimeoutError, the remaining items use fetchItemManifest(..., { cacheOnly: true }), which serves a cached copy (even a stale one) or skips the item. A blackholed network still costs one 10 s wait, not one per batch of 32.
  • One warning: skipped 199 items ("a", "b", "c", ...): <first reason>, or the old single-item line when only one item fails.

Evidence

A local HTTPS registry built from this repo's registry/ that releases one response every 40 ms (a slow shared link), with an empty cache:

0.8.169 this branch
wall time 10.4 s 18.0 s (the link's floor: 447 x 40 ms)
TimeoutError lines on stderr 199 0
items searched (total) 248 of 447 447 of 447

The real registry over a healthy connection with an empty cache: 0.8-1.3 s before, 0.94-0.99 s after (3 runs each), 447 items either way.

Tests

Two new cases in registry/resolver.test.ts:

  • 100 entries with a fetch that yields once: at most 32 are ever in flight. On main 100 are.
  • 100 entries whose fetch throws TimeoutError: only the first 32 reach the network, and exactly one summary warning is printed. On main there are 100 fetches.

With only the cache-only fallback reverted, the second case fails (100 calls). Both passed 3 runs in a row. The registry, catalog and moved-helper suites pass (335 tests).

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 187 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@terencecho terencecho 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.

Reviewed exact HEAD b0c1399252ae4c8fe39999d4c7b091e9d30a08be against effective BASE b74686ef8e2680ee1f4d623cad92d1a8218f8746 (API BASE 2b9009ab520a31fa7f23cbad44583733a6602abc).

No must-fix blocker found. The extracted helper preserves the existing algorithm and positional results, and the registry callback isolates item failures rather than allowing them to reject the shared pool.

Important, nonblocking — stale-cache timeouts bypass the new fuse

packages/cli/src/registry/resolver.ts:59–63, with packages/cli/src/registry/remote.ts:162–172: the fuse only observes a rejected fetchItemManifest. A successful prior catalog run can populate valid item caches; after 24 hours, an offline/blackholed connection causes revalidation to time out, but fetchItemManifest returns each stale cached item from its catch. Consequently timedOut stays false and every queued row still attempts the network.

The bounded pool therefore introduces ceil(N / 32) timeout waves on that ordinary stale-cache path, versus the effective BASE's single concurrent wave. For 96 stale items, that is three configured 10-second windows rather than one, before catalog.ts:238 can continue to filtering/search/output. This is source-derived scheduling/arithmetic, not measured wall time or an executed reproduction. Both versions eventually return the same valid cached items; I am treating the regression as important/nonblocking, not a wrong-data or permanent-liveness blocker.

Follow-up: preserve stale-data success while propagating timeout observation separately to the per-load fuse. Add a case that primes more than 32 item caches through the normal fetch/write path, expires them, then rejects revalidation with TimeoutError; assert all cached results survive without additional network cohorts. The new resolver tests use empty caches, while the existing remote stale-fallback test does not exercise the pool. The PR body's one-timeout-window claim does not currently cover stale caches.

Scope: full changed effective-BASE source read before full changed HEAD source, applicable standards/current review rubric, current public history, actual catalog/cache/transport paths, helper/Lambda integration, and every pair of bundled behavioral groups. Two independent source-only passes considered the same stale-cache issue. The final successor changes only the helper JSDoc. Untouched source interiors and platform/dependency implementation remain outside this bounded audit. No repository/dependency code, tests, builds, browser/native work, benchmarks, or CI were executed or queried; author test/performance claims remain unverified.

Verdict: APPROVE
Reasoning: No demonstrated must-fix correctness, security, persistence, or permanent-liveness defect remains in the reviewed scope. The stale-cache hot-path latency regression above is an important follow-up and does not certify the claimed offline timing.

— Review by tai (pr-review)

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at 6c56be4d. The previous approval was tai's at b0c13992, so I reviewed the whole PR rather than only the delta.

  • Delta. b0c13992..6c56be4d changes only the doc comment on runWithConcurrencyLimit in utils/concurrency.ts, which now wraps onto two lines. No code changed.
  • Helper move. The body of runWithConcurrencyLimit is identical to the copy removed from lambda/render-batch.ts. render-batch.ts and its test now import it from utils/concurrency.js.
  • Bounded fetching. loadAllItems runs up to 32 fetches at a time, and each request starts its own 10 s AbortSignal.timeout inside fetchJson. So a timeout now covers only that request's transfer, not time spent waiting in a queue.
  • Fuse. The first TimeoutError sets timedOut. After that, fetchItemManifest(..., { cacheOnly: true }) returns any cached copy, stale or not, or throws without touching the network. The fetch-failed-with-cache path is unchanged.
  • Warning. One summary line, or the old single-item line when only one item fails.
  • Tests. The new resolver tests pin the in-flight cap at 32, and check that only the first wave of 32 reaches the network after a timeout.
  • Not blocking. I agree with tai's follow-up: when the cache is stale, fetchItemManifest returns the cached copy instead of throwing the timeout, so the fuse never trips. An offline run with a stale cache can wait through several 10 s waves before it ends.

All three commits are signed and verified. CI: 53 checks pass, none pending or failing. No live run on my side.

— Rames

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 95a686c Oct 18, 2026
68 checks passed
@miguel-heygen
miguel-heygen deleted the scrubfix/catalogfix branch October 18, 2026 08:51
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