Skip to content

fix(brief): bundle resvg linux-x64-gnu native binding with carousel fn#3204

Merged
koala73 merged 2 commits into
mainfrom
fix/carousel-native-binding-bundle
Apr 19, 2026
Merged

fix(brief): bundle resvg linux-x64-gnu native binding with carousel fn#3204
koala73 merged 2 commits into
mainfrom
fix/carousel-native-binding-bundle

Conversation

@koala73

@koala73 koala73 commented Apr 19, 2026

Copy link
Copy Markdown
Owner

The real root cause

Every single Telegram brief carousel attempt since PR #3174 merged has failed in production. PR #3196 (my middleware fix) addressed a theoretical UA-block path but the observed failure was actually deeper: the Vercel function itself crashes hard on cold start with HTTP 500 FUNCTION_INVOCATION_FAILED on every request — even OPTIONS — meaning the isolate cannot initialise.

Reproduced just now, ~4h after PR #3196 merged:

curl -I -H "User-Agent: TelegramBot (like TwitterBot)" \
  "https://www.worldmonitor.app/api/brief/carousel/user_test/2026-04-19/0"
HTTP/2 500
x-vercel-error: FUNCTION_INVOCATION_FAILED

OPTIONS / GET / HEAD — all 500. This is isolate boot failure, not a render error (which would 503 from our handler) and not middleware (which would 403 JSON).

Why it crashes

api/brief/carousel/[userId]/[issueDate]/[page].ts imports server/_shared/brief-carousel-render.ts, which lazy-imports @resvg/resvg-js. That package's js-binding.js does:

// node_modules/@resvg/resvg-js/js-binding.js
// ...
case "linux":
  switch (arch) {
    case "x64":
      // ...
      nativeBinding = require("@resvg/resvg-js-linux-x64-gnu");

A conditional require of a peer-optional package picked at runtime from process.platform + process.arch + isMusl(). On Vercel Lambda (Amazon Linux 2 glibc) that resolves to @resvg/resvg-js-linux-x64-gnu.

Vercel's @vercel/build node-file-trace does NOT follow this pattern — the optional peer is declared in package.json's optionalDependencies but isn't pulled into the function's output bundle. At cold start the require() throws MODULE_NOT_FOUND, the isolate crashes during module evaluation, Vercel returns the generic FUNCTION_INVOCATION_FAILED page, and Telegram's sendMediaGroup reports it as WEBPAGE_CURL_FAILED (same failure mode we've been chasing since the feature shipped).

Fix

Force the binding into the function's deploy bundle via vercel.json functions.includeFiles. Only the carousel route needs this — every other api/* route is unaffected.

"functions": {
  "api/brief/carousel/[userId]/[issueDate]/[page].ts": {
    "includeFiles": "node_modules/@resvg/resvg-js-linux-x64-gnu/**"
  }
}

Files

File Change
vercel.json Add functions.includeFiles rule scoping the linux-x64-gnu native binding to the carousel function only

Tests

  • npm run typecheck clean (no TS change)
  • tests/deploy-config.test.mjs 21/21 pass
  • node -e validates the JSON shape
  • Reproduced the 500 locally via curl -I across all methods and UAs
  • Cross-referenced node_modules/@resvg/resvg-js/js-binding.js to confirm linux-x64-gnu is the exact subpackage Amazon Linux 2 will require at runtime

Post-deploy validation

curl -I -H "User-Agent: TelegramBot (like TwitterBot)" \\
  "https://www.worldmonitor.app/api/brief/carousel/<userId>/<date>/0?t=<token>"
# Expect: HTTP/2 200, content-type: image/png
# Before this PR: HTTP/2 500 FUNCTION_INVOCATION_FAILED

Then wait for the next digest cron tick (30-min cadence) and confirm Railway log line [digest] Telegram carousel 400 does NOT reappear. On the Telegram recipient side the 3-image album should finally land above the text.

Why the previous fix matters too

PR #3196 (middleware carousel UA bypass) is still valid defence-in-depth. Without it, once the function itself starts returning 200 image/png, Telegram's fetcher would then be 403'd by BOT_UA. So both PRs together are what actually unblock the feature end-to-end. Merge order doesn't matter (both changes are independent and safe).

Follow-ups (not in this PR)

  • The font fetch from jsdelivr is still a runtime dep; if it ever flakes the render 503s. Worth bundling the TTF inline as a follow-up. Out of scope for this hotfix.
  • The brief footer link in Telegram digests is still truncated off the 4096-char message. Separate issue — move footer to top of telegramText in buildChannelBodies.

Test plan

  • deploy-config tests pass
  • JSON valid
  • Post-merge: curl -I with TelegramBot UA returns 200
  • Post-merge: next cron tick confirms carousel lands in Telegram

Real root cause of every Telegram carousel WEBPAGE_CURL_FAILED
since PR #3174 merged. Not middleware (last PR fixed that
theoretical path but not the observed failure). The Vercel
function itself crashes HTTP 500 FUNCTION_INVOCATION_FAILED on
every request including OPTIONS - the isolate can't initialise.

The handler imports brief-carousel-render which lazy-imports
@resvg/resvg-js. That package's js-binding.js does runtime
require(@resvg/resvg-js-<platform>-<arch>-<libc>). On Vercel
Lambda (Amazon Linux 2 glibc) that resolves to
@resvg/resvg-js-linux-x64-gnu. Vercel nft tracing does NOT
follow this conditional require so the optional peer package
isnt bundled. Cold start throws MODULE_NOT_FOUND, isolate
crashes, Vercel returns FUNCTION_INVOCATION_FAILED, Telegram
reports WEBPAGE_CURL_FAILED.

Fix: vercel.json functions.includeFiles forces linux-x64-gnu
binding into the carousel functions bundle. Only this route
needs it; every other api route is unaffected.

Verified:
- deploy-config tests 21/21 pass
- JSON valid
- Reproduced 500 via curl on all methods and UAs
- resvg-js/js-binding.js confirms linux-x64-gnu is the runtime
  binary on Amazon Linux 2 glibc

Post-merge: curl with TelegramBot UA should return 200 image/png
instead of 500; next cron tick should clear the Railway
[digest] Telegram carousel 400 line.
@vercel

vercel Bot commented Apr 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Apr 19, 2026 9:33am

Request Review

@greptile-apps

greptile-apps Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a production cold-start crash on the Vercel carousel function by adding a functions.includeFiles entry in vercel.json. Vercel's node-file-trace bundler does not follow the runtime process.platform/process.arch conditional require() in @resvg/resvg-js/js-binding.js, so the @resvg/resvg-js-linux-x64-gnu native binding was never included in the deployed bundle — every request returned FUNCTION_INVOCATION_FAILED at isolate boot. The fix is minimal, well-scoped to the one affected route, and correctly targets the Amazon Linux 2 glibc x86_64 binding used by Vercel Lambda.

Confidence Score: 5/5

Safe to merge — single-line config change with clear root-cause analysis and no breaking side-effects on other routes.

All remaining findings are P2 style/hardening suggestions (missing regression test, undocumented platform assumption). The fix itself is correct: includeFiles is the established Vercel mechanism for bundling platform-conditional native bindings, the glob targets only the carousel function, and the Amazon Linux 2 x86_64 binding choice is well-evidenced.

No files require special attention. tests/deploy-config.test.mjs would benefit from a regression guard as noted in the inline comment.

Important Files Changed

Filename Overview
vercel.json Adds functions.includeFiles to force the @resvg/resvg-js-linux-x64-gnu native binding into the carousel function bundle, fixing the MODULE_NOT_FOUND cold-start crash on Vercel Lambda (Amazon Linux 2, x86_64).

Sequence Diagram

sequenceDiagram
    participant Telegram as Telegram sendMediaGroup
    participant Vercel as Vercel Lambda
    participant Bundler as Vercel node-file-trace
    participant Binding as @resvg/resvg-js-linux-x64-gnu

    Note over Bundler,Binding: Before this PR
    Bundler->>Binding: Skips conditional require() in js-binding.js
    Vercel-->>Telegram: HTTP 500 FUNCTION_INVOCATION_FAILED (MODULE_NOT_FOUND)

    Note over Bundler,Binding: After this PR (vercel.json includeFiles)
    Bundler->>Binding: Force-includes via includeFiles glob
    Telegram->>Vercel: GET /api/brief/carousel/{userId}/{date}/{page}
    Vercel->>Binding: require('@resvg/resvg-js-linux-x64-gnu') OK
    Vercel-->>Telegram: HTTP 200 image/png
Loading

Comments Outside Diff (1)

  1. tests/deploy-config.test.mjs, line 17 (link)

    P2 No regression guard for the carousel includeFiles rule

    The deploy-config.test.mjs suite validates all other critical vercel.json settings (caching, CSP, security headers) but doesn't assert that the carousel functions.includeFiles entry is present and points to the correct binding. Without a test, a future vercel.json tidy-up or accidental revert would silently re-introduce the MODULE_NOT_FOUND cold-start crash with no CI signal. A minimal assertion would guard against this regression:

    it('carousel function bundles the resvg linux-x64-gnu native binding', () => {
      const carouselFn = vercelConfig.functions?.['api/brief/carousel/[userId]/[issueDate]/[page].ts'];
      assert.ok(carouselFn, 'Missing functions entry for carousel route');
      assert.equal(
        carouselFn.includeFiles,
        'node_modules/@resvg/resvg-js-linux-x64-gnu/**',
        'includeFiles must point to the linux-x64-gnu binding so Vercel bundles the native .node file'
      );
    });

Reviews (1): Last reviewed commit: "fix(brief): bundle resvg linux-x64-gnu n..." | Re-trigger Greptile

Comment thread vercel.json
"crons": [],
"functions": {
"api/brief/carousel/[userId]/[issueDate]/[page].ts": {
"includeFiles": "node_modules/@resvg/resvg-js-linux-x64-gnu/**"

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.

P2 Platform architecture assumption undocumented

This fix is correct for Vercel Lambda running on Amazon Linux 2 x86_64 (glibc), but @resvg/resvg-js/js-binding.js also resolves linux-arm64-gnu when process.arch === 'arm64'. If Vercel migrates its Node.js runtime pool to Graviton/arm64 instances (which AWS Lambda supports), the cold-start crash would silently return with no binding included. Adding a brief inline comment here preserves the reasoning for future maintainers and makes the architecture dependency explicit.

Consider documenting this as: // Amazon Linux 2 x86_64 (glibc) — re-verify arch if Vercel migrates to Graviton/arm64

Two P2 findings on PR #3204:

P2 #1 (inline on vercel.json:6): Platform architecture assumption
undocumented. If Vercel migrates to Graviton/arm64 Lambda the
cold-start crash silently returns. vercel.json is strict JSON so
comments aren't possible inline.

P2 #2 (tests/deploy-config.test.mjs:17): No regression guard for
the carousel includeFiles rule. A future vercel.json tidy-up
could silently revert the fix with no CI signal.

Fixed both in a single block:

- New describe() in deploy-config.test.mjs asserts the carousel
  route's functions entry exists AND its includeFiles points at
  @resvg/resvg-js-linux-x64-gnu. Any drift fails the build.
- The block comment above it documents the Amazon Linux 2 x86_64
  glibc assumption that would have lived next to the includeFiles
  entry if JSON supported comments. Includes the Graviton/arm64
  migration pointer.

tests 22/22 pass (was 21, +1 new).
@koala73

koala73 commented Apr 19, 2026

Copy link
Copy Markdown
Owner Author

Addressed both P2 findings in 2371eeb.

P2 #1 — arch-assumption reasoning. vercel.json is strict JSON (no comments), so the reasoning that would have lived inline now sits in a block comment above the new regression test. Calls out the Amazon Linux 2 x86_64 glibc assumption and the Graviton/arm64 migration case explicitly.

P2 #2 — regression guard. New describe('brief carousel function native-binding bundling') in tests/deploy-config.test.mjs asserts:

  1. vercelConfig.functions['api/brief/carousel/[userId]/[issueDate]/[page].ts'] exists (guard against an accidental tidy-up deleting it)
  2. includeFiles equals exactly 'node_modules/@resvg/resvg-js-linux-x64-gnu/**' (guard against drift off the binding Vercel Lambda actually loads)

Both assertions ship with context-heavy failure messages pointing back to this PR so anyone who trips them in CI gets the full breadcrumb trail (root cause + why this exact glob matters + what to change if Vercel's arch ever shifts).

Verified: tsx --test tests/deploy-config.test.mjs → 22/22 pass (was 21, +1 new).

@koala73
koala73 merged commit 27849fe into main Apr 19, 2026
10 checks passed
@koala73
koala73 deleted the fix/carousel-native-binding-bundle branch April 19, 2026 09:36
koala73 added a commit that referenced this pull request Apr 19, 2026
PR #3204 shipped the right `includeFiles` value but the WRONG key:

  "api/brief/carousel/[userId]/[issueDate]/[page].ts"

Vercel's `functions` config keys are micromatch globs, not literal
paths. Bracketed segments like `[userId]` are parsed as character
classes (match any ONE character from {u,s,e,r,I,d}), so my rule
matched zero files and `includeFiles` was silently ignored. Post-
merge probe still returned HTTP 500 FUNCTION_INVOCATION_FAILED on
every request. Build log shows zero mentions of `carousel` or
`resvg` — corroborates the key never applied.

Fix: wildcard path segments.

  "api/brief/carousel/**"

Matches any file under the carousel route dir. Since the only
deployed file there is the dynamic-segment handler, the effective
scope is identical to what I originally intended.

Added a second regression test that sweeps every functions key and
fails loudly if any bracketed segment slips back in. Guards against
future reverts AND against anyone copy-pasting the literal route
path without realising Vercel reads it as a glob.

23/23 deploy-config tests pass (was 22, +1 new guard).
koala73 added a commit that referenced this pull request Apr 19, 2026
…follow-up) (#3206)

* fix(brief): use wildcard glob in vercel.json functions key

PR #3204 shipped the right `includeFiles` value but the WRONG key:

  "api/brief/carousel/[userId]/[issueDate]/[page].ts"

Vercel's `functions` config keys are micromatch globs, not literal
paths. Bracketed segments like `[userId]` are parsed as character
classes (match any ONE character from {u,s,e,r,I,d}), so my rule
matched zero files and `includeFiles` was silently ignored. Post-
merge probe still returned HTTP 500 FUNCTION_INVOCATION_FAILED on
every request. Build log shows zero mentions of `carousel` or
`resvg` — corroborates the key never applied.

Fix: wildcard path segments.

  "api/brief/carousel/**"

Matches any file under the carousel route dir. Since the only
deployed file there is the dynamic-segment handler, the effective
scope is identical to what I originally intended.

Added a second regression test that sweeps every functions key and
fails loudly if any bracketed segment slips back in. Guards against
future reverts AND against anyone copy-pasting the literal route
path without realising Vercel reads it as a glob.

23/23 deploy-config tests pass (was 22, +1 new guard).

* Address Greptile P2: widen bracket-literal guard regex

Greptile spotted that `/\[[A-Za-z]+\]/` only matches purely-alphabetic
segment names. Real-world Next.js routes often use `[user_id]`,
`[issue_date]`, `[page1]`, `[slug2024]` — none flagged by the old
regex, so the guard would silently pass on the exact kind of
regression it was written to catch.

Widened to `/\[[A-Za-z][A-Za-z0-9_]*\]/`:
  - requires a leading letter (so legit char classes like `[0-9]`
    and `[!abc]` don't false-positive)
  - allows letters, digits, underscores after the first char
  - covers every Next.js-style dynamic-segment name convention

Also added a self-test that pins positive cases (userId, user_id,
issue_date, page1, slug2024) and negative cases (the actual `**`
glob, `[0-9]`, `[!abc]`) so any future narrowing of the regex
breaks CI immediately instead of silently re-opening PR #3206.

24/24 deploy-config tests pass (was 23, +1 new self-test).
koala73 added a commit that referenced this pull request Apr 19, 2026
Every attempt to ship the Phase 8 Telegram carousel on Vercel's
Node serverless runtime has failed at cold start:

- PR #3174 direct satori + @resvg/resvg-wasm: Vercel edge bundler
  refused the `?url` asset import required by resvg-wasm.
- PR #3174 (fix) direct satori + @resvg/resvg-js native binding:
  Node runtime accepted it, but Vercel's nft tracer does not follow
  @resvg/resvg-js/js-binding.js's conditional
  `require('@resvg/resvg-js-<platform>-<arch>-<libc>')` pattern,
  so the linux-x64-gnu peer package was never bundled. Cold start
  threw MODULE_NOT_FOUND, isolate crashed,
  FUNCTION_INVOCATION_FAILED on every request including OPTIONS,
  and Telegram reported WEBPAGE_CURL_FAILED with no other signal.
- PR #3204 added `vercel.json` `functions.includeFiles` to force
  the binding in, but (a) the initial key was a literal path that
  Vercel micromatch read as a character class (PR #3206 fixed),
  (b) even with the corrected `api/brief/carousel/**` wildcard, the
  function still 500'd across the board. The `functions.includeFiles`
  path appears honored in the deployment manifest but not at runtime
  for this particular native-binding pattern.

Fix: swap the renderer to @vercel/og's ImageResponse, which is
Vercel's first-party wrapper around satori + resvg-wasm with
Vercel-native bundling. Runs on Edge runtime — matches every other
API route in the project. No native binding, no includeFiles, no
nft tracing surprises. Cold start ~300ms, warm ~30ms.

Changes:
- server/_shared/brief-carousel-render.ts: replace renderCarouselPng
  (Uint8Array) with renderCarouselImageResponse (ImageResponse).
  Drop ensureLibs + satori + @resvg/resvg-js dynamic-import dance.
  Keep layout builders (buildCover/buildThreads/buildStory) and
  font loading unchanged — the Satori object trees are
  wire-compatible with ImageResponse.
- api/brief/carousel/[userId]/[issueDate]/[page].ts: flip
  `runtime: 'nodejs'` -> `runtime: 'edge'`. Delegate rendering to
  the renderer's ImageResponse and return it directly; error path
  still 503 no-store so CDN + Telegram don't pin a bad render.
- vercel.json: drop the now-useless `functions.includeFiles` block.
- package.json: drop direct `@resvg/resvg-js` and `satori` deps
  (both now bundled inside @vercel/og).
- tests/deploy-config.test.mjs: replace the native-binding
  regression guards with an assertion that no `functions` block
  exists (with a comment pointing at the skill documenting the
  micromatch gotcha for future routes).
- tests/brief-carousel.test.mjs: updated comment references.

Verified:
- typecheck + typecheck:api clean
- test:data 5814/5814 pass
- node -e test: @vercel/og imports cleanly in Node (tests that
  reach through the renderer file no longer depend on native
  bindings)

Post-deploy validation:
  curl -I -H "User-Agent: TelegramBot (like TwitterBot)" \
    "https://www.worldmonitor.app/api/brief/carousel/<uid>/<slot>/0"
  # Expect: HTTP/2 403 (no token) or 200 (valid token)
  # NOT:    HTTP/2 500 FUNCTION_INVOCATION_FAILED

Then tail Railway digest logs on the next tick; the
`[digest] Telegram carousel 400 ... WEBPAGE_CURL_FAILED` line
should stop appearing, and the 3-image preview should actually land
on Telegram.
koala73 added a commit that referenced this pull request Apr 19, 2026
* fix(brief): switch carousel to @vercel/og on edge runtime

Every attempt to ship the Phase 8 Telegram carousel on Vercel's
Node serverless runtime has failed at cold start:

- PR #3174 direct satori + @resvg/resvg-wasm: Vercel edge bundler
  refused the `?url` asset import required by resvg-wasm.
- PR #3174 (fix) direct satori + @resvg/resvg-js native binding:
  Node runtime accepted it, but Vercel's nft tracer does not follow
  @resvg/resvg-js/js-binding.js's conditional
  `require('@resvg/resvg-js-<platform>-<arch>-<libc>')` pattern,
  so the linux-x64-gnu peer package was never bundled. Cold start
  threw MODULE_NOT_FOUND, isolate crashed,
  FUNCTION_INVOCATION_FAILED on every request including OPTIONS,
  and Telegram reported WEBPAGE_CURL_FAILED with no other signal.
- PR #3204 added `vercel.json` `functions.includeFiles` to force
  the binding in, but (a) the initial key was a literal path that
  Vercel micromatch read as a character class (PR #3206 fixed),
  (b) even with the corrected `api/brief/carousel/**` wildcard, the
  function still 500'd across the board. The `functions.includeFiles`
  path appears honored in the deployment manifest but not at runtime
  for this particular native-binding pattern.

Fix: swap the renderer to @vercel/og's ImageResponse, which is
Vercel's first-party wrapper around satori + resvg-wasm with
Vercel-native bundling. Runs on Edge runtime — matches every other
API route in the project. No native binding, no includeFiles, no
nft tracing surprises. Cold start ~300ms, warm ~30ms.

Changes:
- server/_shared/brief-carousel-render.ts: replace renderCarouselPng
  (Uint8Array) with renderCarouselImageResponse (ImageResponse).
  Drop ensureLibs + satori + @resvg/resvg-js dynamic-import dance.
  Keep layout builders (buildCover/buildThreads/buildStory) and
  font loading unchanged — the Satori object trees are
  wire-compatible with ImageResponse.
- api/brief/carousel/[userId]/[issueDate]/[page].ts: flip
  `runtime: 'nodejs'` -> `runtime: 'edge'`. Delegate rendering to
  the renderer's ImageResponse and return it directly; error path
  still 503 no-store so CDN + Telegram don't pin a bad render.
- vercel.json: drop the now-useless `functions.includeFiles` block.
- package.json: drop direct `@resvg/resvg-js` and `satori` deps
  (both now bundled inside @vercel/og).
- tests/deploy-config.test.mjs: replace the native-binding
  regression guards with an assertion that no `functions` block
  exists (with a comment pointing at the skill documenting the
  micromatch gotcha for future routes).
- tests/brief-carousel.test.mjs: updated comment references.

Verified:
- typecheck + typecheck:api clean
- test:data 5814/5814 pass
- node -e test: @vercel/og imports cleanly in Node (tests that
  reach through the renderer file no longer depend on native
  bindings)

Post-deploy validation:
  curl -I -H "User-Agent: TelegramBot (like TwitterBot)" \
    "https://www.worldmonitor.app/api/brief/carousel/<uid>/<slot>/0"
  # Expect: HTTP/2 403 (no token) or 200 (valid token)
  # NOT:    HTTP/2 500 FUNCTION_INVOCATION_FAILED

Then tail Railway digest logs on the next tick; the
`[digest] Telegram carousel 400 ... WEBPAGE_CURL_FAILED` line
should stop appearing, and the 3-image preview should actually land
on Telegram.

* Add renderer smoke test + fix Cache-Control duplication

Reviewer flagged residual risk: no dedicated carousel-route smoke
test for the @vercel/og path. Adds one, and catches a real bug in
the process.

Findings during test-writing:

1. @vercel/og's ImageResponse runs CLEANLY in Node via tsx — the
   comment in brief-carousel.test.mjs saying "we can't test the
   render in Node" was true for direct satori + @resvg/resvg-wasm
   but no longer holds after PR #3210. Pure Node render works
   end-to-end: satori tree-parse, jsdelivr font fetch, resvg-wasm
   init, PNG output. ~850ms first call, ~20ms warm.

2. ImageResponse sets its own default
   `Cache-Control: public, immutable, no-transform, max-age=31536000`.
   Passing Cache-Control via the constructor's headers option
   APPENDS rather than overrides, producing a duplicated
   comma-joined value like
   `public, immutable, no-transform, max-age=31536000, public, max-age=60`
   on the Response. The route handler was doing exactly this via
   extraHeaders. Fix: drop our Cache-Control override and rely on
   @vercel/og's 1-year immutable default — envelope is only
   immutable for its 7d Redis TTL so the effective ceiling is 7d
   anyway (after that the route 404s before render).

Changes:

- tests/brief-carousel.test.mjs: 6 new assertions under
  `renderCarouselImageResponse`:
    * renders cover / threads / story pages, each returning a
      valid PNG (magic bytes + size range)
    * rejects a structurally empty envelope
    * threads non-cache extraHeaders onto the Response
    * pins @vercel/og's Cache-Control default so it survives
      caller-supplied Cache-Control overrides (regression guard
      for the bug fixed in this commit)
- api/brief/carousel/[userId]/[issueDate]/[page].ts: remove the
  stacked Cache-Control; lean on @vercel/og default. Drop the now-
  unused `PAGE_CACHE_TTL` constant. Comment explains why.

Verified:
- test:data 5820/5820 pass (was 5814, +6 smoke)
- typecheck + typecheck:api clean
- Render smoke: cover 825ms / threads 23ms / story 16ms first run
  (wasm init dominates first render)
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.

1 participant