Skip to content

fix: prevent memory leaks by explicitly disposing WASM contexts and d…#2457

Merged
PupilTong merged 2 commits intolynx-family:mainfrom
PupilTong:p/hw/free-lynx-view-instance
Apr 14, 2026
Merged

fix: prevent memory leaks by explicitly disposing WASM contexts and d…#2457
PupilTong merged 2 commits intolynx-family:mainfrom
PupilTong:p/hw/free-lynx-view-instance

Conversation

@PupilTong
Copy link
Copy Markdown
Collaborator

@PupilTong PupilTong commented Apr 14, 2026

…etaching event listeners during component teardown

Summary by CodeRabbit

  • Bug Fixes
    • Prevented memory leaks by ensuring event listeners and related resources are fully released when views are removed.
    • Improved disposal ordering to guarantee native bindings and background work are cleaned up, reducing use-after-free and hanging shutdown scenarios.

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).
  • Changeset added, and when a BREAKING CHANGE occurs, it needs to be clearly marked (or not required).

…etaching event listeners during component teardown
@PupilTong PupilTong requested a review from Sherry-hue as a code owner April 14, 2026 06:49
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 14, 2026

🦋 Changeset detected

Latest commit: 9802421

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@lynx-js/web-core Patch
upgrade-rspeedy Patch
@lynx-js/web-rsbuild-server-middleware Patch
@lynx-js/template-webpack-plugin Patch
@lynx-js/react-rsbuild-plugin Patch
create-rspeedy Patch
@lynx-js/web-worker-rpc Patch
@lynx-js/react-alias-rsbuild-plugin Patch
@lynx-js/rspeedy Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

This PR adds coordinated async disposal across the web-core client: serialized component disposal, explicit WASM binding/context freeing, event-listener tracking and removal, and guards to prevent operations after disposal to eliminate WASM memory leaks and use-after-free risks.

Changes

Cohort / File(s) Summary
Changeset
.changeset/fix-wasmcontext-memory-leak.md
Adds patch changeset documenting the fix for WASM context memory leak and strict FIFO async disposal.
LynxView lifecycle
packages/web-platform/web-core/ts/client/mainthread/LynxView.ts
Introduces async #disposeInstance() with serialized disposal via #disposePromise, marks disconnected state, awaits disposal before rendering, clears shadowRoot and resets adopted stylesheets.
LynxViewInstance cleanup ordering
packages/web-platform/web-core/ts/client/mainthread/LynxViewInstance.ts
Ensures mtsWasmBinding.dispose() is called during Symbol.asyncDispose before awaiting background thread shutdown.
WASM binding — listeners & dispose
packages/web-platform/web-core/ts/client/mainthread/elementAPIs/WASMJSBinding.ts
Adds normalized-event deduplication for addEventListener, and a new dispose() that removes tracked listeners, clears internal sets, calls optional disposeWasmContext, and nulls wasmContext.
Element API — dispose hook & guards
packages/web-platform/web-core/ts/client/mainthread/elementAPIs/createElementAPI.ts
Changes wasmContext to let, installs mtsBinding.disposeWasmContext to free the WASM pointer and null captured state, adds disposed guard used by idle/timing flush to skip work after disposal.
Background thread disposal guard
packages/web-platform/web-core/ts/client/mainthread/Background.ts
Makes disposal conditional on whether BTS was started to avoid awaiting readiness/dispose RPC when not started (prevents potential hang).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested reviewers

  • Sherry-hue
  • hzy

Poem

🐰
I tidy up listeners, one hop at a time,
Free the WASM pointer, keep memory sublime.
FIFO disposal, neat threads unwind,
No dangling echoes, no ghosts left behind. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objectives of the PR: preventing memory leaks through explicit WASM context disposal and event listener detachment during component teardown, which aligns with all file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 22.85714% with 54 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...platform/web-core/ts/client/mainthread/LynxView.ts 0.00% 29 Missing ⚠️
.../ts/client/mainthread/elementAPIs/WASMJSBinding.ts 30.43% 16 Missing ⚠️
.../client/mainthread/elementAPIs/createElementAPI.ts 38.46% 8 Missing ⚠️
.../web-core/ts/client/mainthread/LynxViewInstance.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/web-platform/web-core/ts/client/mainthread/elementAPIs/createElementAPI.ts`:
- Around line 77-89: The dispose function (mtsBinding.disposeWasmContext) nulls
shared bindings (wasmContext, mtsBinding, rootDom, page, timingFlags) while
async callbacks queued by __FlushElementTree (via requestIdleCallbackImpl which
calls mtsBinding.postTimingFlags(...) and wasmContext.gc()) still close over
those variables; this can cause TypeError. Fix by replacing the nullification
with a disposed flag (e.g., set disposed = true) and avoid mutating the captured
bindings, or capture stable locals for any work you enqueue (create const
localMts = mtsBinding, localWasm = wasmContext before scheduling callbacks) and
in queued callbacks check disposed before using them; also clear/zero
timingFlags safely (e.g., timingFlags.length = 0 only after ensuring no queued
work will read it). Ensure changes touch mtsBinding.disposeWasmContext and any
code paths in __FlushElementTree/requestIdleCallbackImpl that rely on those
bindings.

In `@packages/web-platform/web-core/ts/client/mainthread/LynxView.ts`:
- Around line 419-420: The awaited call to oldInstance[Symbol.asyncDispose]() in
LynxView blocks renders when disposal can hang (as noted for backgroundThread
when startBTS() wasn't called); change LynxView to not await the asyncDispose on
oldInstance (e.g., fire-and-forget the Promise), or run it with a bounded
timeout/guard and catch errors so disposal cannot block subsequent renders;
locate the disposal call in the LynxView class (oldInstance and its
Symbol.asyncDispose usage) and update it to invoke asyncDispose without awaiting
(or wrap it in a cancellable/timed helper) and ensure any rejection is
caught/logged.
- Around line 407-425: The teardown race occurs because `#disposeInstance` clears
this.#instance and then awaits oldInstance[Symbol.asyncDispose](), and
disconnectedCallback doesn't await it, so a fast reconnect can create a new
instance that gets wiped when the old dispose resumes; fix by serializing
disposes with an in-flight token or promise (e.g., add this.#disposePromise or
this.#disposeGeneration) so `#disposeInstance` records a unique generation/promise
before awaiting Symbol.asyncDispose and only clears or mutates shadowRoot if the
generation matches, and ensure the render/attach logic (where a new instance is
created) awaits any existing this.#disposePromise (or checks generation) before
reusing shadowRoot or assigning this.#instance to avoid stale teardown wiping a
new render.

In `@packages/web-platform/web-core/ts/client/mainthread/LynxViewInstance.ts`:
- Around line 299-301: The disposal order is wrong: free the background thread
before disposing the main-thread WASM binding to avoid use-after-free during RPC
teardown; in the async disposal method (async [Symbol.asyncDispose]()) call
await this.backgroundThread[Symbol.asyncDispose]() first, then call
this.mtsWasmBinding.dispose() (and clear any references to mtsWasmBinding
afterward) so backgroundThread teardown can complete while the WASM context is
still valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5aac50a2-8475-4c37-a81c-6c6ba84c30f2

📥 Commits

Reviewing files that changed from the base of the PR and between a9f8d05 and e5e0079.

📒 Files selected for processing (5)
  • .changeset/fix-wasmcontext-memory-leak.md
  • packages/web-platform/web-core/ts/client/mainthread/LynxView.ts
  • packages/web-platform/web-core/ts/client/mainthread/LynxViewInstance.ts
  • packages/web-platform/web-core/ts/client/mainthread/elementAPIs/WASMJSBinding.ts
  • packages/web-platform/web-core/ts/client/mainthread/elementAPIs/createElementAPI.ts

Comment thread packages/web-platform/web-core/ts/client/mainthread/LynxView.ts
Comment thread packages/web-platform/web-core/ts/client/mainthread/LynxView.ts Outdated
@PupilTong PupilTong self-assigned this Apr 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web-platform/web-core/ts/client/mainthread/LynxView.ts (1)

469-510: ⚠️ Potential issue | 🟡 Minor

#rendering flag may not reset when #url is falsy.

If this.#url is not set when #render() is called, the if block (lines 469-510) is skipped, but #rendering remains true (set on line 450). This blocks all subsequent #render() calls even after url is later assigned.

🐛 Proposed fix
         const mtsRealm = await mtsRealmPromise;
         if (this.#url) {
           // ... existing code ...
           this.#instance = await lynxViewInstance;
-          this.#rendering = false;
         }
+        this.#rendering = false;
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web-platform/web-core/ts/client/mainthread/LynxView.ts` around lines
469 - 510, The `#rendering` flag can remain true when this.#url is falsy because
the if (this.#url) block is skipped; modify the render method (the `#render` /
render function where this.#rendering is set true) to ensure this.#rendering is
reset to false whenever rendering cannot proceed: either add an early branch
that sets this.#rendering = false and returns when this.#url is falsy, or wrap
the async work in a try/finally and set this.#rendering = false in the finally
block, referencing the same private field names (`#rendering`, `#url`) and the
lynxViewInstance/this.#instance logic so the flag is always cleared on exit.
🧹 Nitpick comments (1)
packages/web-platform/web-core/ts/client/mainthread/LynxView.ts (1)

507-508: Consider moving lynxGroupId declaration before its closure capture.

The lynxGroupId variable is used on line 488 inside the .then() callback but declared on line 507. While this works correctly (the declaration executes before the callback runs due to the await on line 508), it's confusing. Moving the declaration before line 470 would improve readability.

♻️ Suggested reordering
+        const lynxGroupId = this.lynxGroupId;
         const lynxViewInstance = import(
           /* webpackChunkName: "web-core-main-chunk" */
           /* webpackFetchPriority: "high" */
           './LynxViewInstance.js'
         ).then(({ LynxViewInstance }) => {
           ...
         });
         templateManager.fetchBundle(
           ...
         );
-        const lynxGroupId = this.lynxGroupId;
         this.#instance = await lynxViewInstance;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web-platform/web-core/ts/client/mainthread/LynxView.ts` around lines
507 - 508, The variable lynxGroupId is declared after its use inside the .then()
callback that resolves lynxViewInstance which is confusing; move the declaration
of lynxGroupId so it appears before the closure that references it (i.e., above
the code that calls or awaits lynxViewInstance and before the .then() callback),
ensuring the .then() closure reads the already-declared lynxGroupId; keep the
assignment to this.#instance = await lynxViewInstance unchanged but relocate the
lynxGroupId declaration to precede that await/.then() usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/web-platform/web-core/ts/client/mainthread/LynxView.ts`:
- Around line 469-510: The `#rendering` flag can remain true when this.#url is
falsy because the if (this.#url) block is skipped; modify the render method (the
`#render` / render function where this.#rendering is set true) to ensure
this.#rendering is reset to false whenever rendering cannot proceed: either add
an early branch that sets this.#rendering = false and returns when this.#url is
falsy, or wrap the async work in a try/finally and set this.#rendering = false
in the finally block, referencing the same private field names (`#rendering`,
`#url`) and the lynxViewInstance/this.#instance logic so the flag is always
cleared on exit.

---

Nitpick comments:
In `@packages/web-platform/web-core/ts/client/mainthread/LynxView.ts`:
- Around line 507-508: The variable lynxGroupId is declared after its use inside
the .then() callback that resolves lynxViewInstance which is confusing; move the
declaration of lynxGroupId so it appears before the closure that references it
(i.e., above the code that calls or awaits lynxViewInstance and before the
.then() callback), ensuring the .then() closure reads the already-declared
lynxGroupId; keep the assignment to this.#instance = await lynxViewInstance
unchanged but relocate the lynxGroupId declaration to precede that await/.then()
usage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2d8f69c3-3cca-4607-b2a0-14bab045a9ec

📥 Commits

Reviewing files that changed from the base of the PR and between e5e0079 and 9802421.

📒 Files selected for processing (3)
  • packages/web-platform/web-core/ts/client/mainthread/Background.ts
  • packages/web-platform/web-core/ts/client/mainthread/LynxView.ts
  • packages/web-platform/web-core/ts/client/mainthread/elementAPIs/createElementAPI.ts

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 14, 2026

Merging this PR will degrade performance by 26.66%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 2 regressed benchmarks
✅ 78 untouched benchmarks
⏩ 21 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
002-hello-reactLynx-destroyBackground 681.9 µs 929.7 µs -26.66%
transform 1000 view elements 43.7 ms 40.7 ms +7.47%
basic-performance-scroll-view-100 8.1 ms 8.7 ms -6.33%

Comparing PupilTong:p/hw/free-lynx-view-instance (9802421) with main (a9f8d05)

Open in CodSpeed

Footnotes

  1. 21 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@relativeci
Copy link
Copy Markdown

relativeci bot commented Apr 14, 2026

React MTF Example

#402 Bundle Size — 206.12KiB (0%).

9802421(current) vs a9f8d05 main#384(baseline)

Bundle metrics  no changes
                 Current
#402
     Baseline
#384
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 173 173
No change  Duplicate Modules 67 67
No change  Duplicate Code 45.79% 45.79%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#402
     Baseline
#384
No change  IMG 111.23KiB 111.23KiB
No change  Other 94.89KiB 94.89KiB

Bundle analysis reportBranch PupilTong:p/hw/free-lynx-view-in...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci bot commented Apr 14, 2026

React External

#387 Bundle Size — 591.76KiB (0%).

9802421(current) vs a9f8d05 main#369(baseline)

Bundle metrics  no changes
                 Current
#387
     Baseline
#369
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 17 17
No change  Duplicate Modules 5 5
No change  Duplicate Code 8.59% 8.59%
No change  Packages 0 0
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#387
     Baseline
#369
No change  Other 591.76KiB 591.76KiB

Bundle analysis reportBranch PupilTong:p/hw/free-lynx-view-in...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci bot commented Apr 14, 2026

React Example

#7269 Bundle Size — 236.83KiB (0%).

9802421(current) vs a9f8d05 main#7251(baseline)

Bundle metrics  no changes
                 Current
#7269
     Baseline
#7251
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 179 179
No change  Duplicate Modules 70 70
No change  Duplicate Code 46.13% 46.13%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#7269
     Baseline
#7251
No change  IMG 145.76KiB 145.76KiB
No change  Other 91.07KiB 91.07KiB

Bundle analysis reportBranch PupilTong:p/hw/free-lynx-view-in...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci bot commented Apr 14, 2026

Web Explorer

#8843 Bundle Size — 749.26KiB (+0.08%).

9802421(current) vs a9f8d05 main#8825(baseline)

Bundle metrics  Change 4 changes Regression 1 regression
                 Current
#8843
     Baseline
#8825
Regression  Initial JS 44.45KiB(+0.39%) 44.27KiB
No change  Initial CSS 2.16KiB 2.16KiB
Change  Cache Invalidation 15.35% 0%
No change  Chunks 8 8
No change  Assets 10 10
Change  Modules 148(-1.33%) 150
No change  Duplicate Modules 11 11
Change  Duplicate Code 35.02%(+0.03%) 35.01%
No change  Packages 3 3
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#8843
     Baseline
#8825
No change  Other 401.63KiB 401.63KiB
Regression  JS 345.47KiB (+0.17%) 344.87KiB
No change  CSS 2.16KiB 2.16KiB

Bundle analysis reportBranch PupilTong:p/hw/free-lynx-view-in...Project dashboard


Generated by RelativeCIDocumentationReport issue

@PupilTong
Copy link
Copy Markdown
Collaborator Author

@coderabbitai reconsider your comments with latest PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch all review comments made by coderabbitai on PR `#2457`
gh pr view 2457 --repo lynx-family/lynx-stack --json reviews,comments

echo "---REVIEW_COMMENTS---"
gh api repos/lynx-family/lynx-stack/pulls/2457/comments --paginate | jq '[.[] | select(.user.login == "coderabbitai") | {id: .id, path: .path, line: .line, body: .body, diff_hunk: .diff_hunk}]'

echo "---ISSUE_COMMENTS---"
gh api repos/lynx-family/lynx-stack/issues/2457/comments --paginate | jq '[.[] | select(.user.login == "coderabbitai") | {id: .id, body: .body[0:500]}]'

Length of output: 72649


Let me fetch the current code to properly reassess each comment.


🏁 Script executed:

#!/bin/bash
echo "=== LynxView.ts ==="
cat packages/web-platform/web-core/ts/client/mainthread/LynxView.ts

echo ""
echo "=== LynxViewInstance.ts ==="
cat packages/web-platform/web-core/ts/client/mainthread/LynxViewInstance.ts

echo ""
echo "=== createElementAPI.ts ==="
cat packages/web-platform/web-core/ts/client/mainthread/elementAPIs/createElementAPI.ts

echo ""
echo "=== Background.ts ==="
cat packages/web-platform/web-core/ts/client/mainthread/Background.ts

Length of output: 53571


@PupilTong: I've reviewed all my prior comments against the latest code. Here's my updated assessment:


✅ Resolved / Withdrawn Comments

1. createElementAPI.ts — binding nullification risk (originally flagged)
The current code correctly uses a disposed boolean flag. The requestIdleCallbackImpl callback checks if (disposed) return before calling mtsBinding.postTimingFlags(...) and wasmContext.gc(). The captured bindings are not nulled — only disposed is toggled. This concern is resolved.

2. LynxView.tsasyncDispose blocking renders
The fix in Background.ts now guards await this.#btsReady and rpc.invoke(disposeEndpoint) behind if (this.#btsStarted), eliminating the potential hang when BTS was never started. This concern is substantially mitigated.

3. LynxView.ts — teardown race on fast reconnect
The #disposePromise mechanism correctly serializes disposal and re-render. When #render() sees this.#disposePromise is set, it awaits #disposeInstance() (which returns the existing in-flight promise). The shadowRoot.innerHTML = '' completes before createIFrameRealm runs. This concern is resolved.

4. LynxViewInstance.ts — disposal order (WASM before background thread)
I was wrong on this one. Calling mtsWasmBinding.dispose() first is actually the safer order: it removes all rootDom event listeners so no racing RPC responses can trigger WASM operations on an already-disposed context. The subsequent backgroundThread[Symbol.asyncDispose]() only needs to send an RPC message to the worker and terminate it — the WASM context does not need to be alive for that. I withdraw this comment.


⚠️ Still Valid Concerns

5. LynxView.ts#rendering flag not reset when #url is falsy
This remains a real bug. Inside queueMicrotask, this.#rendering = false is only set within the if (this.#url) block. If url is not yet set when #render() fires (e.g. from connectedCallback before the url property is assigned), #rendering is permanently stuck at true, silently blocking all future renders. A simple fix:

queueMicrotask(async () => {
  // ...
  const mtsRealm = await mtsRealmPromise;
  if (this.#url) {
    // ... create instance ...
    this.#instance = await lynxViewInstance;
  }
  this.#rendering = false; // Move outside the if block
});

6. LynxView.tslynxGroupId declared after its closure (nitpick)
const lynxGroupId = this.lynxGroupId is declared after the .then() callback that references it. This works at runtime (the closure captures the let/const binding slot, and the declaration executes synchronously before the async import resolves), but it creates a confusing TDZ-like appearance. Moving the declaration before the import(...) call would improve readability.


🧠 Learnings used
Learnt from: gaoachao
Repo: lynx-family/lynx-stack PR: 1782
File: packages/react/transform/crates/swc_plugin_inject/napi.rs:31-47
Timestamp: 2025-09-19T07:37:58.778Z
Learning: User gaoachao prefers to keep refactoring PRs minimal and focused, deferring non-essential improvements to separate PRs to maintain clear scope boundaries.

@PupilTong PupilTong merged commit 7332eb4 into lynx-family:main Apr 14, 2026
73 of 78 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants