fix(core): scope prompt cache entries to version and label
What is the current behavior?
VoltOpsPromptManagerImpl.getCacheKey() builds the key from the prompt name and version only,
defaulting to "latest" when version is absent. PromptReference.label is ignored.
label is a supported selector (@voltagent/core types and VoltOpsPromptApiClient.buildPromptUrl
both use it), so { promptName: "greeting", label: "production" } and
{ promptName: "greeting" } map to the same key. Whichever call stores first wins the cache entry
(default TTL: 5 minutes); the other selector is then served that prompt's content and metadata.
Two different labels (production / staging) likewise share one entry.
Reproduction (mock API client, same pattern as the existing spec):
getPrompt({ promptName: "greeting", label: "production" })-> returns "Production prompt", caches it.getPrompt({ promptName: "greeting" })-> expected "Latest prompt", actually gets the cached
"Production prompt" with no second API call.
What is the new behavior?
Cache keys now include the selector that was requested:
name:version:1forversion: 1name:label:productionforlabel: "production"name:latestfor no selector
Version still takes precedence over label, matching PromptReference's documented contract. Repeating
the same selector still hits the cache, so latency is unchanged for the common case. The parts are
escaped before joining, so a prompt name containing : cannot forge a label or version namespace.
Version selectors now use name:version:v where they previously produced name:v. That changes
getCacheStats().entries for version lookups; the cache is an in-memory Map, so there is no persisted
key to migrate.
Tests
packages/core/src/voltops/prompt-manager.spec.ts, 39/39 passing.
- Regression test for the reported bug:
should not reuse a cached prompt for a different label.
It fails onmainwithexpected 'Production prompt' to be 'Latest prompt'and passes with the fix.
It also asserts per-selector cache hits, so caching was not disabled for labelled prompts. - Invariant test for the escaping:
should not collide for prompt names that contain the selector delimiter. Its behavioural assertions already pass onmain; theentriesassertion pins the new
key format and fails there, because the base revision buildsx:label:y:latestfor the first lookup
andx:latestfor the second. - Selector precedence and edge cases:
should prefer an explicit version over a label when both are suppliedpins the precedence documented onPromptReference.version, and
should treat an empty label as the unlabelled lookuppins thelabel:empty-string fallback.
pnpm --filter @voltagent/core exec vitest run src/voltops/prompt-manager.spec.ts
-> 39/39 passed
packages/core suite (run inside packages/core with its own vitest.config.mts): 1265 passed, 5 failed,
2 skipped, 1 todo — the 5 failures are the pre-existing Windows-only src/workspace/sandbox ones
(local.spec.ts, toolkit.spec.ts) and come from the environment, not from this diff.
tsc --noEmit and biome are clean.
Changeset
.changeset/quiet-labels-cache.md -> @voltagent/core: patch.
Checklist
- Read the contributing guide
- Changeset added (
@voltagent/core, patch) - Tests covering the change
- Core package test suite run locally (39/39)
- GitHub Actions matrix (Node 20/22/24) — the workflow is
action_requiredfor a first-time
contributor, so a maintainer needs to approve it
Notes for reviewers
The key shape for explicit version selectors changes from name:v to name:version:v. That is
visible in getCacheStats().entries; the cache itself is in-memory only, so nothing is persisted and
there is no migration. If you would rather keep name:v for versions and only namespace labels, say so
and I will restructure the diff.
Fixes #1520
Summary by cubic
Fixes prompt cache collisions when selecting by label. VoltOpsPromptManagerImpl.getCacheKey() now keys on the requested selector (version, label, or latest), so labelled prompts no longer reuse entries from a different selector. Key parts are escaped, so prompt names containing : cannot collide with label or version values. Version precedence over label is preserved, an empty label falls back to the unlabelled latest entry, and repeating the same selector still hits the cache. Fixes #1520.
- Version lookups change key shape from
name:v1toname:version:v1, sogetCacheStats().entriesdiffers for version selectors; the cache is in-memory, so no migration is required.
Summary by CodeRabbit
- Bug Fixes
- Prompt requests using different selectors—such as a specific version, a label, or the latest prompt—now keep their cached results separate. An explicit version takes precedence over a label, and an empty label is treated like no label.
- Repeated requests with the same selector can still reuse the cache. Prompt names and selector values containing delimiter characters no longer cause unrelated requests to share a cache entry, keeping cached responses associated with the intended prompt and selection.