fix(core): normalize URLs in ActCache key derivation

Merged
YA
5mo ago
+90-2

Closes #2189.

why

ActCache.buildActCacheKey (packages/core/lib/v3/cache/ActCache.ts:184) hashes the page URL verbatim from page.url() with no normalization. URLs that are semantically equivalent — same origin, path, and set of query parameters — produce different cache keys whenever the parameters appear in a different order. The cache silently misses, triggering an LLM call where one shouldn't be needed.

This is pervasive on real-world URLs: e-commerce links, ad funnels, anywhere utm_* / session params drift between visits.

First runSecond runSame page?Cache hits today?
?id=42&utm_source=email?utm_source=email&id=42yesno
?ref=home&page=2?page=2&ref=homeyesno

what changed

  • New normalizeUrlForCacheKey in cache/utils.ts: parses the URL, calls URLSearchParams.sort(), reserializes. Falls back to the raw input if the URL is unparseable ("", about:blank-like values).
  • ActCache.prepareContext passes the normalized URL into buildActCacheKey while the stored entry.url still holds the raw URL (so on-disk cache files remain debuggable).
  • New unit test packages/core/tests/unit/act-cache-url-normalize.test.ts covering: sort behavior, equivalent URLs producing identical output, fragment preservation, duplicate-key relative-order preservation, fallback paths.

design notes

The normalizer is intentionally conservative — only sorts parameters. Out of scope here, but candidates for follow-ups:

  • Tracking-param stripping (utm_*, fbclid, gclid, etc.) — requires opinions on what counts as tracking; could break intentional differentiation, e.g. a customer that uses ?ref=X as a meaningful state.
  • Fragment stripping — fragments often indicate distinct views in SPAs, so removing them could lose intent.
  • Trailing-slash normalization — some servers treat them differently.

Each of those could land separately once the conservative pass is in.

compatibility

Strict superset of current cache hits — no URL that hits the cache today will miss after this change. URLs that miss today because of param reordering will now hit. The on-disk cache key for any URL containing query params changes; old entries become unreachable (don't break anything, just don't get used), and the next run rewrites them with the new key.

test plan

  • pnpm run build:esm && pnpm exec vitest run --config vitest.esm.config.mjs dist/esm/tests/unit/act-cache-url-normalize.test.js — 9/9 pass.
  • No public API changes; normalizeUrlForCacheKey is exported for testability but lives under cache/ and isn't re-exported from the SDK root.
  • Changeset bundled (@browserbasehq/stagehand patch).

Found while reading ActCache.ts to understand the self-healing flow — happy to follow up with the tracking-param strip or fragment handling as a separate PR once this lands.


Summary by cubic

Normalize URLs in ActCache key derivation so query-parameter order no longer creates different cache keys, increasing hit rate and avoiding extra LLM calls. Closes #2189.

  • Bug Fixes

    • Added normalizeUrlForCacheKey in cache/utils.ts to sort query params via URLSearchParams.sort().
    • prepareContext now hashes the normalized URL; stored entry.url stays raw for debugging.
    • Preserves fragments and duplicate-key order; passes through empty/unparseable/about:blank URLs.
    • Added unit tests for sort behavior and equivalence cases.
  • Migration

    • Cache keys for URLs with query params change; existing on-disk entries won’t be used and will regenerate on next run.