fix(core): normalize URLs in ActCache key derivation
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 run | Second run | Same page? | Cache hits today? |
|---|---|---|---|
?id=42&utm_source=email | ?utm_source=email&id=42 | yes | no |
?ref=home&page=2 | ?page=2&ref=home | yes | no |
what changed
- New
normalizeUrlForCacheKeyincache/utils.ts: parses the URL, callsURLSearchParams.sort(), reserializes. Falls back to the raw input if the URL is unparseable ("",about:blank-like values). ActCache.prepareContextpasses the normalized URL intobuildActCacheKeywhile the storedentry.urlstill holds the raw URL (so on-disk cache files remain debuggable).- New unit test
packages/core/tests/unit/act-cache-url-normalize.test.tscovering: 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=Xas 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;
normalizeUrlForCacheKeyis exported for testability but lives undercache/and isn't re-exported from the SDK root. - Changeset bundled (
@browserbasehq/stagehandpatch).
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
normalizeUrlForCacheKeyincache/utils.tsto sort query params viaURLSearchParams.sort(). prepareContextnow hashes the normalized URL; storedentry.urlstays raw for debugging.- Preserves fragments and duplicate-key order; passes through empty/unparseable/
about:blankURLs. - Added unit tests for sort behavior and equivalence cases.
- Added
-
Migration
- Cache keys for URLs with query params change; existing on-disk entries won’t be used and will regenerate on next run.