Skip to content

fix: do not expire Infinity gcTime via setTimeout overflow - #25

Open
SebTardif wants to merge 1 commit into
openclaw:mainfrom
SebTardif:fix/f003-gctime-overflow
Open

SebTardif wants to merge 1 commit into
openclaw:mainfrom
SebTardif:fix/f003-gctime-overflow

Conversation

@SebTardif

Copy link
Copy Markdown

What Problem This Solves

Fixes an issue where consumers that call createRouter({ gcTime: Infinity }), set preloadGcTime: Infinity, or pass a retention longer than the host timer limit (for example 30 * 24 * 60 * 60 * 1000) would schedule a 1ms garbage-collection timer. Node and browsers coerce setTimeout delays outside the signed 32-bit range to 1ms, so the unused match is re-checked every millisecond and Node prints TimeoutOverflowWarning. staleTime: Infinity is not affected because it is a comparison, not a timer.

Why This Change Was Made

scheduleGc now skips a timer when gcTime or preloadGcTime is not finite, so Infinity keeps the cached match until it is replaced or stop() runs. Finite values above 2^31-1 ms are clamped to that limit. The existing callback still reschedules when the match is younger than gcTime, so a 30-day retention can expire later instead of spinning 1ms timers. No public exports, types, history adapter shape, or loader signature changed.

User Impact

gcTime: Infinity and preloadGcTime: Infinity keep unused cached matches without overflowing the host timer. A 30-day (or other overflow) retention no longer emits TimeoutOverflowWarning or wake the event loop every millisecond. Default 30-minute GC is unchanged. Apps that never pass Infinity or a delay above about 24.8 days behave as before.

Evidence

Live node against the built dist/index.js on this branch, macOS 26.6.2, Node v26.7.0. The same public sequence (createRouter({ gcTime }), navigate("chat"), navigate("fast"), then 20ms) was run against the unpatched bundle from upstream/main and the patched bundle.

Unpatched dist/index.js (gcTime: Infinity, 16 overflow warnings in 20ms):

$ node /tmp/proof-uirouter-f003.mjs /tmp/uirouter-f003-unpatched.js Infinity
(node:41839) TimeoutOverflowWarning: Infinity does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
GCTIME Infinity
WARNINGS 16
WARNING0 TimeoutOverflowWarning: Infinity does not fit into a 32-bit signed integer.
CACHED_AFTER_NAV 1
CACHED_AFTER_20MS 1
CACHED_ROUTE chat
ACTIVE_ROUTE fast

Unpatched dist/index.js (gcTime: 30 * 24 * 60 * 60 * 1000):

$ node /tmp/proof-uirouter-f003.mjs /tmp/uirouter-f003-unpatched.js 30d
(node:41840) TimeoutOverflowWarning: 2592000000 does not fit into a 32-bit signed integer.
Timeout duration was set to 1.
GCTIME 2592000000
WARNINGS 18
WARNING0 TimeoutOverflowWarning: 2592000000 does not fit into a 32-bit signed integer.
CACHED_AFTER_NAV 1
CACHED_AFTER_20MS 1
CACHED_ROUTE chat
ACTIVE_ROUTE fast

Patched dist/index.js (gcTime: Infinity, no overflow warning):

$ node /tmp/proof-uirouter-f003.mjs /tmp/oc-pr-uirouter-F003/dist/index.js Infinity
GCTIME Infinity
WARNINGS 0
WARNING0 none
CACHED_AFTER_NAV 1
CACHED_AFTER_20MS 1
CACHED_ROUTE chat
ACTIVE_ROUTE fast

Patched dist/index.js (gcTime: 2592000000, no overflow warning):

$ node /tmp/proof-uirouter-f003.mjs /tmp/oc-pr-uirouter-F003/dist/index.js 30d
GCTIME 2592000000
WARNINGS 0
WARNING0 none
CACHED_AFTER_NAV 1
CACHED_AFTER_20MS 1
CACHED_ROUTE chat
ACTIVE_ROUTE fast

pnpm run check passed locally (format, typecheck, lint, 23 tests, pack/import).

This timer path has been in scheduleGc since f047b64a87a5 (2026-06-20, refactor: finalize router match loading). Same class of host-timer overflow: TanStack Query #6287 (TimeoutOverflowWarning in Query.scheduleGc). Adjacent but different: #20 (stale navigation cancellation), #21 (loader redirect hop cap), #24 (history listener context).

Real behavior proof

  • Behavior or issue addressed: createRouter({ gcTime: Infinity }) and a 30-day gcTime no longer overflow setTimeout into a 1ms GC timer (TimeoutOverflowWarning). Unused cached matches stay cached without a 1ms reschedule loop.

  • Real environment tested: macOS 26.6.2 arm64, Node v26.7.0, @openclaw/uirouter built from fix/f003-gctime-overflow at /tmp/oc-pr-uirouter-F003.

  • Exact steps or command run after this patch:

    cd /tmp/oc-pr-uirouter-F003
    node /tmp/proof-uirouter-f003.mjs /tmp/oc-pr-uirouter-F003/dist/index.js Infinity
    node /tmp/proof-uirouter-f003.mjs /tmp/oc-pr-uirouter-F003/dist/index.js 30d
  • Evidence after fix: terminal output from the patched dist/index.js:

    $ node /tmp/proof-uirouter-f003.mjs /tmp/oc-pr-uirouter-F003/dist/index.js Infinity
    GCTIME Infinity
    WARNINGS 0
    WARNING0 none
    CACHED_AFTER_NAV 1
    CACHED_AFTER_20MS 1
    CACHED_ROUTE chat
    ACTIVE_ROUTE fast
    $ node /tmp/proof-uirouter-f003.mjs /tmp/oc-pr-uirouter-F003/dist/index.js 30d
    GCTIME 2592000000
    WARNINGS 0
    WARNING0 none
    CACHED_AFTER_NAV 1
    CACHED_AFTER_20MS 1
    CACHED_ROUTE chat
    ACTIVE_ROUTE fast
  • Observed result after fix: After createRouter({ gcTime: Infinity }) (and the 30-day value) plus navigate("chat") then navigate("fast"), Node emitted 0 TimeoutOverflowWarning events and cachedMatches still held chat after 20ms. The same commands against the unpatched bundle emitted 16 and 18 overflow warnings (Timeout duration was set to 1).

  • What was not tested: A browser setTimeout in a running OpenClaw UI shell, and a per-route gcTime: Infinity override (same scheduleGc helper).

Host setTimeout coerces Infinity and delays above 2^31-1 ms to 1ms.
createRouter({ gcTime: Infinity }) and a 30-day gcTime therefore
scheduled a 1ms GC timer (TimeoutOverflowWarning) and rescheduled
it every millisecond.

Skip non-finite gcTime so those matches stay cached. Clamp finite
overflows to 2^31-1 ms so the existing reschedule path can expire
them later.

Signed-off-by: Sebastien Tardif <[email protected]>
@SebTardif
SebTardif requested a review from a team as a code owner August 30, 2026 01:43
@clawsweeper

clawsweeper Bot commented Aug 30, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Aug 30, 2026
@clawsweeper

clawsweeper Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex review: blocked before merge. Reviewed September 5, 2026, 12:00 PM ET / 16:00 UTC.

ClawSweeper review

What this changes

The PR prevents infinite and oversized cache-retention periods from overflowing host timers, with documentation and three regression tests.

Regression provenance

Possible regression — suspected (reviewed change). No predecessor PR is attributed.

Merge readiness

Blocked before merge - 4 items remain

The overflow fix remains necessary on main and has convincing Node proof, but both previously reported cache-lifetime defects remain in the unchanged head.

Priority: P2
Reviewed head: b981a9c73817a4b9ab5b1a538bec9d95d4e2f934

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) Strong before/after runtime evidence supports a useful, focused fix, but two concrete cache-lifetime regressions block merge.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The supplied macOS/Node traces exercise the built router through navigation and show overflow warnings falling from 16/18 to zero for Infinity/30-day retention while cached data remains present; the compatibility findings require separate regression coverage.
Patch quality 🦐 gold shrimp (3/6) 2 actionable review findings remain.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The supplied macOS/Node traces exercise the built router through navigation and show overflow warnings falling from 16/18 to zero for Infinity/30-day retention while cached data remains present; the compatibility findings require separate regression coverage.
Evidence reviewed 11 items Repository policy and boundary: Read the full root AGENTS.md and applied its cache-lifecycle, resource-cleanup, compatibility, and package-validation guidance. No nested AGENTS.md or matching maintainer notes were present. Repository identity was confirmed through origin and package.json.
Verified introduced change: The pinned base-to-head diff introduces the non-finite early return and bounded timer delay. Fetched-main changes leave src, test, and README unchanged relative to the pinned base; no regression claim relies on the stale test-merge candidate.
Current main still needs the fix: Main passes remaining retention directly to setTimeout and reschedules while the match is younger than gcTime. Infinity and oversized finite durations therefore still reach the host timer without a guard or clamp.
Findings 2 actionable findings [P1] Restrict non-expiring retention to positive Infinity
[P2] Cancel an existing GC timer before returning for Infinity
Security None None.

How this fits together

The standalone UI router loads routes supplied by applications and caches their components and data. Its garbage-collection scheduler decides when unused or preloaded matches leave that cache.

flowchart TD
  A[Navigation or preload] --> B[Route loading]
  B --> C[Cached route matches]
  C --> D{Retention duration}
  D -->|Infinite| E[Keep without timer]
  D -->|Finite| F[Bounded timer]
  F --> G[Recheck age or remove match]
Loading

Before merge

  • Restrict non-expiring retention to positive Infinity (P1) - With gcTime: -Infinity or NaN, navigating away from a loaded route now retains its cached match indefinitely; the same change affects preloadGcTime. Previously negative Infinity caused immediate removal, and NaN expired through the timer callback. This silently changes existing configurations to permanent retention and can accumulate entries for every dependency key. Use gcTime === Infinity and add expiry regressions for both other values. This previously reported blocker remains unfixed.
  • Cancel an existing GC timer before returning for Infinity (P2) - Configure preloadGcTime: 10 and gcTime: Infinity, then preload chat, navigate to it, and navigate away before the preload deadline. The original finite timer survives because this return bypasses clearTimeout; its callback retains the captured 10ms duration and removes the now normally cached match. Cancel and remove the previous timer before returning, and cover this transition. This previously reported blocker remains unfixed.
  • Resolve merge risk (P1) - Upgrade compatibility is not established for NaN/negative-Infinity retention or finite-preload-to-infinite-cache transitions; existing consumers can receive different cache lifetimes.
  • Complete next step (P2) - Repair the Infinity guard and obsolete-timer cancellation, add the two regression groups, and run the repository quality gate.

Findings

  • [P1] Restrict non-expiring retention to positive Infinity — src/loading.ts:103-105
  • [P2] Cancel an existing GC timer before returning for Infinity — src/loading.ts:103-105
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production versus test LOC production +7 net; tests +125 Production growth is small and justified by timer overflow handling; three added tests cover the primary cases.

Merge-risk options

Maintainer options:

  1. Preserve existing cache lifetimes (recommended)
    Narrow the Infinity guard and cancel prior timers, with regressions covering expiry inputs and preload-to-cache transitions.
Copy recommended automerge instruction
@clawsweeper automerge

Special instructions:
Restrict timer-free retention to positive Infinity, clear and remove any existing timer before returning, and add public-router regressions for NaN and negative-Infinity expiry plus finite-preload-to-infinite-cache retention; preserve finite-delay clamping and run the repository checks.

Technical review

Best possible solution:

Keep the bounded-delay scheduler, reserve permanent retention for positive Infinity, and cancel obsolete timers whenever retention changes.

Do we have a high-confidence way to reproduce the issue?

Yes: current-main source passes Infinity and 30-day durations directly to setTimeout, matching the supplied before/after Node traces. Both remaining defects also have concrete public-router sequences, but were not executed during this review.

Is this the best way to solve the issue?

Not yet: clamping each timer while retaining age-based rescheduling is appropriate, but the early return must preserve other expiry behavior and cancel an existing timer.

Full review comments:

  • [P1] Restrict non-expiring retention to positive Infinity — src/loading.ts:103-105
    With gcTime: -Infinity or NaN, navigating away from a loaded route now retains its cached match indefinitely; the same change affects preloadGcTime. Previously negative Infinity caused immediate removal, and NaN expired through the timer callback. This silently changes existing configurations to permanent retention and can accumulate entries for every dependency key. Use gcTime === Infinity and add expiry regressions for both other values. This previously reported blocker remains unfixed.
    Confidence: 0.99
  • [P2] Cancel an existing GC timer before returning for Infinity — src/loading.ts:103-105
    Configure preloadGcTime: 10 and gcTime: Infinity, then preload chat, navigate to it, and navigate away before the preload deadline. The original finite timer survives because this return bypasses clearTimeout; its callback retains the captured 10ms duration and removes the now normally cached match. Cancel and remove the previous timer before returning, and cover this transition. This previously reported blocker remains unfixed.
    Confidence: 0.99

Overall correctness: patch is incorrect
Overall confidence: 0.98

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against ea06377b0e80.

Labels

Label justifications:

  • P2: This is a bounded router-cache repair affecting infinite or unusually long retention settings.
  • merge-risk: 🚨 compatibility: The introduced early return changes expiry for other non-finite values and leaves finite preload timers able to override infinite retention.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (terminal): The supplied macOS/Node traces exercise the built router through navigation and show overflow warnings falling from 16/18 to zero for Infinity/30-day retention while cached data remains present; the compatibility findings require separate regression coverage.
  • proof: sufficient: Contributor real behavior proof is sufficient. The supplied macOS/Node traces exercise the built router through navigation and show overflow warnings falling from 16/18 to zero for Infinity/30-day retention while cached data remains present; the compatibility findings require separate regression coverage.

Evidence

Acceptance criteria:

  • [P1] pnpm run test -- test/router-loading.test.ts.
  • [P1] pnpm run check.
  • [P1] git diff --check.
  • [P1] git diff --stat.

What I checked:

  • Repository policy and boundary: Read the full root AGENTS.md and applied its cache-lifecycle, resource-cleanup, compatibility, and package-validation guidance. No nested AGENTS.md or matching maintainer notes were present. Repository identity was confirmed through origin and package.json. (AGENTS.md:1, b981a9c73817)
  • Verified introduced change: The pinned base-to-head diff introduces the non-finite early return and bounded timer delay. Fetched-main changes leave src, test, and README unchanged relative to the pinned base; no regression claim relies on the stale test-merge candidate. (src/loading.ts:103, b981a9c73817)
  • Current main still needs the fix: Main passes remaining retention directly to setTimeout and reschedules while the match is younger than gcTime. Infinity and oversized finite durations therefore still reach the host timer without a guard or clamp. (src/loading.ts:97, ea06377b0e80)
  • Latest release has the same timer implementation: The supplied latest release v0.1.1 resolves locally to f5ce7c0. Its loading.ts is unchanged on fetched main, so neither revision contains this overflow repair. (src/loading.ts, f5ce7c0d7c04)
  • Non-finite expiry regression: The new predicate returns for NaN and negative Infinity as well as positive Infinity. Previously negative Infinity reached immediate removal, while NaN reached a timer callback whose age comparison was false and removed the entry. (src/loading.ts:103, b981a9c73817)
  • Finite preload timer survives infinite retention: Navigation promotes a preloaded match, clears its preload flag, and later caches it under the same match identity. The new Infinity return bypasses cancellation of its previous preload timer, whose callback still compares against the captured finite duration and can remove the recached entry. (src/router.ts:372, b981a9c73817)

Likely related people:

  • Shakker: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Limit permanent retention to positive Infinity and cover NaN and negative-Infinity expiry for cached and preloaded matches.
  • Cancel prior timers when switching to infinite retention and add a finite-preload-to-infinite-cache regression.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (14 earlier review cycles; latest 8 shown)
  • reviewed 2026-09-01T10:04:07.246Z sha b981a9c :: needs changes before merge. :: [P1] Restrict permanent retention to positive Infinity
  • reviewed 2026-09-01T22:06:31.395Z sha b981a9c :: needs changes before merge. :: [P1] Restrict permanent retention to positive Infinity
  • reviewed 2026-09-02T17:19:22.012Z sha b981a9c :: needs changes before merge. :: [P1] Restrict permanent retention to positive Infinity
  • reviewed 2026-09-03T06:03:57.933Z sha b981a9c :: blocked before merge. :: [P1] Restrict permanent retention to positive Infinity
  • reviewed 2026-09-03T14:00:45.148Z sha b981a9c :: blocked before merge. :: [P1] Limit permanent retention to positive Infinity
  • reviewed 2026-09-04T03:59:50.797Z sha b981a9c :: blocked before merge. :: [P1] Limit permanent retention to positive Infinity
  • reviewed 2026-09-04T21:54:00.723Z sha b981a9c :: blocked before merge. :: [P1] Limit permanent retention to positive Infinity
  • reviewed 2026-09-05T10:00:21.758Z sha b981a9c :: blocked before merge. :: [P1] Restrict non-expiring retention to positive Infinity | [P2] Cancel an existing GC timer before returning for Infinity

@clawsweeper clawsweeper Bot added P1 Urgent regression or broken agent/channel workflow affecting real users now. merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. P2 Normal priority bug or improvement with limited blast radius. and removed P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. P1 Urgent regression or broken agent/channel workflow affecting real users now. labels Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant