Skip to content

fix(editor): persist versioned prompt fields during SDK updates#17088

Open
Akash504-ai wants to merge 4 commits into
mastra-ai:mainfrom
Akash504-ai:fix/prompt-update-versioned-fields
Open

fix(editor): persist versioned prompt fields during SDK updates#17088
Akash504-ai wants to merge 4 commits into
mastra-ai:mainfrom
Akash504-ai:fix/prompt-update-versioned-fields

Conversation

@Akash504-ai

@Akash504-ai Akash504-ai commented May 26, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes an issue where editor.prompt.update() only persisted thin prompt block fields like status and metadata, while versioned snapshot fields were ignored during SDK updates.

The following fields now correctly persist through the SDK update flow:

  • name
  • description
  • content
  • rules
  • requestContextSchema

The fix adds prompt-specific update handling that creates a new prompt version snapshot when editable prompt fields change, matching existing Studio/API behavior while preserving current publishing/version semantics.

Related issue(s)

Fixes #17081

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test update

Checklist

  • I have linked the related issue(s) in the description above
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works
  • I have addressed all Coderabbit comments on this PR

ELI5

When someone updates a prompt through the Mastra SDK, important fields like the prompt's name, content, rules, and requestContextSchema weren't being saved. This PR makes those edits persist by creating a new prompt version when those fields change, matching Studio/API behavior.

Overview

Fixes issue #17081 by implementing prompt-specific update handling in the SDK so versioned snapshot fields (name, description, content, rules, requestContextSchema) are persisted when updated via editor.prompt.update(). Previously only thin record fields (status, metadata) were written. Now the SDK auto-creates a new prompt version when editable snapshot fields change, preserving existing publishing/version semantics and recording changedFields metadata. Version creation is retried on common uniqueness/version-number conflict errors to handle concurrent/retry scenarios.

Changes Made

  • packages/editor/src/namespaces/prompt.ts

    • Added PROMPT_BLOCK_SNAPSHOT_CONFIG_FIELDS constant and utilities:
      • deepEqual — recursive equality check
      • extractConfigFromVersion — pull snapshot config from a version
      • getProvidedConfigFields / getProvidedRecordFields — identify provided inputs
      • getChangedFields — compute which config fields actually changed
      • isVersionNumberConflictError — detect uniqueness/version-number conflict errors
    • Added createPromptBlockVersionWithRetry which:
      • fetches latest version, computes changed fields, creates a new version with incremented versionNumber, changedFields, and changeMessage "Auto-saved after edit"
      • retries on version-number uniqueness conflicts (with small backoff)
    • Overrode EditorPromptNamespace.update to:
      • validate storage & promptBlocks domain existence and block presence
      • create a new draft version when provided config fields differ from the latest saved config
      • apply record field updates (authorId/activeVersionId/metadata/status) when present
      • evict local cache and return the resolved draft (status: 'draft')
      • throw if storage or resolution is unavailable
  • packages/editor/src/namespaces/prompt.test.ts

    • Added a Vitest test verifying SDK updates persist snapshot fields:
      • Creates an in-memory prompt, updates name/content/rules/requestContextSchema via SDK
      • Asserts the store.update path is not invoked for snapshot-only changes (updateSpy not called)
      • Asserts the returned resolved object and persisted record reflect changes
      • Confirms a new version was created and changedFields equals ['name','content','rules','requestContextSchema']
      • Asserts activeVersionId remains undefined for the draft update
  • .changeset/sweet-ghosts-lick.md

    • Patch changeset documenting the fix for @mastra/editor.

Notes on behavior & alignment

  • SDK now matches Studio/API behavior: changes to snapshot fields produce a new version but do not auto-activate (publishing semantics preserved).
  • changedFields metadata is recorded for audit/version history.
  • Version creation is retried on detected uniqueness/version-number conflicts to reduce race-related failures.

Review Change Stack

@changeset-bot

changeset-bot Bot commented May 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cde2750

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@mastra/editor Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented May 26, 2026

Copy link
Copy Markdown

@Akash504-ai is attempting to deploy a commit to the Mastra Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e15310ea-3d1b-4808-a2c9-118bcd99bb59

📥 Commits

Reviewing files that changed from the base of the PR and between e6ec40e and cde2750.

📒 Files selected for processing (1)
  • packages/editor/src/namespaces/prompt.ts

Walkthrough

Fixes programmatic editor.prompt.update() so provided prompt snapshot config fields (name, content, rules, requestContextSchema) are persisted by computing config deltas and auto-creating a new version when changes are detected. Adds test coverage and a changeset documenting the patch.

Changes

Prompt block update persistence fix

Layer / File(s) Summary
Config delta computation helpers
packages/editor/src/namespaces/prompt.ts
Adds PROMPT_BLOCK_SNAPSHOT_CONFIG_FIELDS and helpers (deepEqual, extractConfigFromVersion, getProvidedConfigFields, getChangedFields) to extract and compare prompt snapshot config fields.
Version creation with retry
packages/editor/src/namespaces/prompt.ts
Implements createPromptBlockVersionWithRetry to compute changed config fields, create an incremented version with changedFields and message Auto-saved after edit, and retry on uniqueness/version-number conflicts.
Update override with persistence and cache resolution
packages/editor/src/namespaces/prompt.ts
Overrides EditorPromptNamespace.update() to validate storage and prompt existence, trigger auto-version creation when provided config fields differ, apply provided record updates, evict cache, resolve and cache the updated draft, and throw if resolution fails.
Behavior tests for SDK update
packages/editor/src/namespaces/prompt.test.ts
Adds a Vitest test using InMemoryStore that creates a prompt, updates snapshot fields via the SDK, and asserts returned and persisted fields, version count (2), first version's changedFields, and updated.activeVersionId is undefined.
Release changeset
.changeset/sweet-ghosts-lick.md
Patch changeset targeting @mastra/editor noting that prompt block SDK updates now persist editable fields.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • YujohnNattrass
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main fix: ensuring versioned prompt fields persist during SDK updates, directly addressing the linked issue #17081.
Linked Issues check ✅ Passed The PR fully addresses issue #17081 by implementing SDK update handling for name, description, content, rules, and requestContextSchema fields through prompt version snapshots.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the prompt update issue: adding version creation logic, implementing snapshot field handling, and including test coverage for the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: lockfile failed supply-chain policy check. Run pnpm install locally to update the lockfile.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/editor/src/namespaces/prompt.test.ts (2)

29-64: ⚡ Quick win

Cover description in this regression test too.

The fix adds description to the versioned fields, but this test never updates or asserts it. Add one description change here so the new path is exercised end-to-end.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/editor/src/namespaces/prompt.test.ts` around lines 29 - 64, The test
for prompt.update/getById omits the new versioned field description; modify the
update payload passed to prompt.update({ id: 'sdk-updatable-block', ... }) to
include description: e.g. a changed string, and add corresponding assertions:
expect(updated.description).toBe(...) and
expect(persisted!.description).toBe(...). This will exercise the new description
path alongside name/content/rules/requestContextSchema in the existing test.

66-69: ⚡ Quick win

Avoid relying on implicit version ordering in the assertion.

This assumes the updated snapshot is always versions[0]. It is safer to request explicit sort order or assert against the version with versionNumber === 2.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/editor/src/namespaces/prompt.test.ts` around lines 66 - 69, The test
is relying on implicit ordering of versions when asserting on
versions.versions[0] — update the test that calls promptStore.listVersions({
blockId: 'sdk-updatable-block' }) to either pass an explicit sort/order option
to listVersions (so the updated snapshot is guaranteed first) or locate the
specific version object by filtering versions.versions for version.versionNumber
=== 2 and assert its changedFields equals
['name','content','rules','requestContextSchema']; reference promptStore,
listVersions, and versions in your change.
packages/editor/src/namespaces/prompt.ts (1)

149-155: ⚡ Quick win

Preserve the base hydration path here.

CrudEditorNamespace.update() hydrates before caching/returning, but this override stores resolved directly. That makes prompt updates diverge from the base contract and from getById() as soon as hydration does real work.

♻️ Suggested fix
     const resolved = await store.getByIdResolved(input.id, { status: 'draft' });
     if (!resolved) {
       throw new Error(`Failed to resolve entity ${input.id} after update`);
     }
 
-    this._cache.set(input.id, resolved);
-    return resolved;
+    const hydrated = await this.hydrate(resolved);
+    this._cache.set(input.id, hydrated);
+    return hydrated;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/editor/src/namespaces/prompt.ts` around lines 149 - 155, The
override in CrudEditorNamespace.update() bypasses the base hydration path by
caching and returning the raw result from store.getByIdResolved; instead, ensure
the updated entity is passed through the same hydration used by getById() before
caching/returning: after obtaining resolved from store.getByIdResolved, call the
namespace's hydration/getById flow (e.g., invoke this.getById(resolved.id) or
the existing hydrate method) and then set the hydrated object into this._cache
and return that hydrated value so update() matches the base contract and
getById().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/editor/src/namespaces/prompt.ts`:
- Around line 127-143: The update currently calls store.update(...) then
separately calls store.createVersion(...), risking partial persistence if
createVersion fails; wrap both operations in a single storage transaction (e.g.
store.transaction or a new store.updateWithVersion API) so the thin record
update and the optional version insert happen atomically: inside the transaction
compute providedConfig via getProvidedConfigFields(input), fetch latestVersion
via store.getLatestVersion (or tx.getLatestVersion), compute previousConfig and
changedFields, then perform tx.update(input) and, only if changedFields.length >
0, tx.createVersion(...) so either both succeed or both are rolled back.

---

Nitpick comments:
In `@packages/editor/src/namespaces/prompt.test.ts`:
- Around line 29-64: The test for prompt.update/getById omits the new versioned
field description; modify the update payload passed to prompt.update({ id:
'sdk-updatable-block', ... }) to include description: e.g. a changed string, and
add corresponding assertions: expect(updated.description).toBe(...) and
expect(persisted!.description).toBe(...). This will exercise the new description
path alongside name/content/rules/requestContextSchema in the existing test.
- Around line 66-69: The test is relying on implicit ordering of versions when
asserting on versions.versions[0] — update the test that calls
promptStore.listVersions({ blockId: 'sdk-updatable-block' }) to either pass an
explicit sort/order option to listVersions (so the updated snapshot is
guaranteed first) or locate the specific version object by filtering
versions.versions for version.versionNumber === 2 and assert its changedFields
equals ['name','content','rules','requestContextSchema']; reference promptStore,
listVersions, and versions in your change.

In `@packages/editor/src/namespaces/prompt.ts`:
- Around line 149-155: The override in CrudEditorNamespace.update() bypasses the
base hydration path by caching and returning the raw result from
store.getByIdResolved; instead, ensure the updated entity is passed through the
same hydration used by getById() before caching/returning: after obtaining
resolved from store.getByIdResolved, call the namespace's hydration/getById flow
(e.g., invoke this.getById(resolved.id) or the existing hydrate method) and then
set the hydrated object into this._cache and return that hydrated value so
update() matches the base contract and getById().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b713e55a-42a6-4576-8aaa-147da7df6cb9

📥 Commits

Reviewing files that changed from the base of the PR and between 4dbeea2 and 7a87508.

📒 Files selected for processing (3)
  • .changeset/sweet-ghosts-lick.md
  • packages/editor/src/namespaces/prompt.test.ts
  • packages/editor/src/namespaces/prompt.ts

Comment on lines +127 to +143
await store.update(input);

const providedConfig = getProvidedConfigFields(input);
const latestVersion = Object.keys(providedConfig).length > 0 ? await store.getLatestVersion(input.id) : null;
const previousConfig = latestVersion ? extractConfigFromVersion(latestVersion) : null;
const changedFields = previousConfig ? getChangedFields(previousConfig, providedConfig) : [];

if (latestVersion && previousConfig && changedFields.length > 0) {
await store.createVersion({
...previousConfig,
...providedConfig,
id: crypto.randomUUID(),
blockId: input.id,
versionNumber: latestVersion.versionNumber + 1,
changedFields,
changeMessage: 'Auto-saved after edit',
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make the snapshot write atomic with the block update.

store.update() persists the thin record first, and createVersion() happens afterward. If version creation fails, this method throws after a partial update and the versioned fields are still stale. This should be one storage-domain operation or a transaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/editor/src/namespaces/prompt.ts` around lines 127 - 143, The update
currently calls store.update(...) then separately calls
store.createVersion(...), risking partial persistence if createVersion fails;
wrap both operations in a single storage transaction (e.g. store.transaction or
a new store.updateWithVersion API) so the thin record update and the optional
version insert happen atomically: inside the transaction compute providedConfig
via getProvidedConfigFields(input), fetch latestVersion via
store.getLatestVersion (or tx.getLatestVersion), compute previousConfig and
changedFields, then perform tx.update(input) and, only if changedFields.length >
0, tx.createVersion(...) so either both succeed or both are rolled back.

@dane-ai-mastra dane-ai-mastra Bot added the complexity: low Low-complexity PR label May 26, 2026
@dane-ai-mastra

dane-ai-mastra Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

PR triage

Linked issue check passed (#17081).

Mastra uses CodeRabbit for automated code reviews. Please address all feedback from CodeRabbit by either making changes to your PR or leaving a comment explaining why you disagree with the feedback. Since CodeRabbit is an AI, it may occasionally provide incorrect feedback.


PR complexity score

Factor Value Score impact
Files changed 3 +6
Lines changed 245 +12
Author merged PRs 10 -10
Test files changed Yes -10
Final score -2

Applied label: complexity: low


Changed test gate

Changed Test Gate is pending. The Changed Test Gate / changed-tests check will update the test label when it completes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/editor/src/namespaces/prompt.ts (1)

190-202: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't bypass authorId filtering on snapshot-only updates.

authorId is part of the storage update contract for multi-tenant filtering, but this path creates a new version before any author-aware update happens — and when the input only contains snapshot fields, store.update() is skipped entirely. That means adapters relying on authorId checks in update() can be bypassed here.

Validate ownership from the fetched block before calling createPromptBlockVersionWithRetry(), or move version creation behind an author-aware storage API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/editor/src/namespaces/prompt.ts` around lines 190 - 202, The code
currently creates a new version via createPromptBlockVersionWithRetry(store,
input.id, providedConfig) before any author-aware update, allowing snapshot-only
updates to bypass authorId checks; fetch the existing block with
store.getById(input.id) and validate its authorId/ownership against
input.authorId (or require a matching authorId) before calling
createPromptBlockVersionWithRetry, or alternatively move the version creation
into an author-aware storage method (e.g., a new store.createVersionWithAuthor
or wrap createPromptBlockVersionWithRetry behind store.update) so that
update()'s multi-tenant authorId filtering is always applied prior to version
creation; ensure getProvidedConfigFields, getProvidedRecordFields, and
store.update paths enforce the same ownership check.
🧹 Nitpick comments (1)
packages/editor/src/namespaces/prompt.test.ts (1)

31-72: ⚡ Quick win

Cover description in this regression test too.

description is one of the versioned prompt fields this PR is restoring, but this test never updates or asserts it. Adding it here would keep the regression coverage aligned with the full contract.

🧪 Suggested test addition
     const updated = await prompt.update({
       id: 'sdk-updatable-block',
       name: 'SDK Updated Block',
+      description: 'Updated description',
       content: 'Updated content',
       rules: {
         operator: 'AND',
         conditions: [{ field: 'role', operator: 'equals', value: 'admin' }],
@@
     expect(updated.name).toBe('SDK Updated Block');
+    expect(updated.description).toBe('Updated description');
     expect(updated.content).toBe('Updated content');
@@
     expect(persisted!.name).toBe('SDK Updated Block');
+    expect(persisted!.description).toBe('Updated description');
     expect(persisted!.content).toBe('Updated content');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/editor/src/namespaces/prompt.test.ts` around lines 31 - 72, The test
updates and asserts several versioned fields but omits the 'description' field;
modify the call to prompt.update (in the block that updates
'sdk-updatable-block') to include description: e.g., set a new description
value, then assert updated.description and persisted!.description match that
value, and update the versions assertion to expect 'description' included in
versions.versions[0]!.changedFields (alongside 'name', 'content', 'rules',
'requestContextSchema') so prompt.update, prompt.getById, and
promptStore!.listVersions checks cover the restored versioned field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/editor/src/namespaces/prompt.ts`:
- Around line 93-99: The duplicate-version error check is missing the space in
"version number", so change the condition that checks
message.includes('versionnumber') to match the actual error text (e.g.,
message.includes('version number')) or use a case-insensitive regex like
/\bversion\s*number\b/i to robustly detect "Version number ... already exists";
update the check near the existing message variable in the code that builds the
duplicate-detection return expression.

---

Outside diff comments:
In `@packages/editor/src/namespaces/prompt.ts`:
- Around line 190-202: The code currently creates a new version via
createPromptBlockVersionWithRetry(store, input.id, providedConfig) before any
author-aware update, allowing snapshot-only updates to bypass authorId checks;
fetch the existing block with store.getById(input.id) and validate its
authorId/ownership against input.authorId (or require a matching authorId)
before calling createPromptBlockVersionWithRetry, or alternatively move the
version creation into an author-aware storage method (e.g., a new
store.createVersionWithAuthor or wrap createPromptBlockVersionWithRetry behind
store.update) so that update()'s multi-tenant authorId filtering is always
applied prior to version creation; ensure getProvidedConfigFields,
getProvidedRecordFields, and store.update paths enforce the same ownership
check.

---

Nitpick comments:
In `@packages/editor/src/namespaces/prompt.test.ts`:
- Around line 31-72: The test updates and asserts several versioned fields but
omits the 'description' field; modify the call to prompt.update (in the block
that updates 'sdk-updatable-block') to include description: e.g., set a new
description value, then assert updated.description and persisted!.description
match that value, and update the versions assertion to expect 'description'
included in versions.versions[0]!.changedFields (alongside 'name', 'content',
'rules', 'requestContextSchema') so prompt.update, prompt.getById, and
promptStore!.listVersions checks cover the restored versioned field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6a0d0a7b-e661-48c6-8354-209e0024a332

📥 Commits

Reviewing files that changed from the base of the PR and between 7a87508 and e6ec40e.

📒 Files selected for processing (2)
  • packages/editor/src/namespaces/prompt.test.ts
  • packages/editor/src/namespaces/prompt.ts

Comment thread packages/editor/src/namespaces/prompt.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: low Low-complexity PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] editor.prompt.update() fails to update core prompt fields programmatically

1 participant