fix(core): scope prompt cache entries to version and label

SH
6d ago
+134-2

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):

  1. getPrompt({ promptName: "greeting", label: "production" }) -> returns "Production prompt", caches it.
  2. 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:1 for version: 1
  • name:label:production for label: "production"
  • name:latest for 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 on main with expected '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 on main; the entries assertion pins the new
    key format and fails there, because the base revision builds x:label:y:latest for the first lookup
    and x:latest for the second.
  • Selector precedence and edge cases: should prefer an explicit version over a label when both are supplied pins the precedence documented on PromptReference.version, and
    should treat an empty label as the unlabelled lookup pins the label: 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_required for 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:v1 to name:version:v1, so getCacheStats().entries differs 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.