Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/experiments/editorial-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Notes are `WP_Comment` objects with `comment_type = 'note'` and `status = 'hold'

- **New thread**: `POST /wp/v2/comments` with `parent: 0` → response `id` stored in `block.attributes.metadata.noteId` via `updateBlockAttributes`
- **Reply**: `POST /wp/v2/comments` with `parent: existingNoteId` → block metadata unchanged (association already set)
- **AI author**: All Notes created by this experiment include `meta: { ai_note: true }`. The `rest_pre_insert_comment` filter intercepts this and sets the author to "WordPress AI" with no email, URL, or user ID, so Notes are not attributed to the authenticated user's account.
- **AI author**: All Notes created by this experiment include `meta: { wpai_note: true }`. The `rest_pre_insert_comment` filter intercepts this and sets the author to "WordPress AI" with no email, URL, or user ID, so Notes are not attributed to the authenticated user's account.
- **Resolved Notes**: Notes with `status = 'approve'` (resolved) cause their associated block to be skipped entirely on the next review run.

## Using the Ability via REST API
Expand Down
4 changes: 2 additions & 2 deletions docs/experiments/summarization.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ When enabled, the Content Summarization experiment adds a "Generate AI Summary"
- One-click summary generation from post content
- Automatically creates a group variation block with the summary
- Summary block can be regenerated from block toolbar
- Summary is saved to post meta (`ai_generated_summary`)
- Summary is saved to post meta (`wpai_generated_summary`)
- Works with any post type that supports the editor

### For Developers
Expand Down Expand Up @@ -330,7 +330,7 @@ The system instruction guides the AI to:

### Post Meta Storage

- The summary is stored in post meta as `ai_generated_summary`
- The summary is stored in post meta as `wpai_generated_summary`
- This meta is registered for the `post` post type and is available in REST API
- The meta is updated each time a summary is generated
- The meta can be accessed programmatically for custom use cases
Expand Down
8 changes: 4 additions & 4 deletions docs/features/image-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ All three abilities can be called directly via REST API, making them useful for
### Key Hooks & Entry Points

- `WordPress\AI\Features\Image_Generation\Image_Generation::register()` wires everything once the feature is enabled:
- `register_post_meta()` → registers `ai_generated` post meta for attachment post type
- `register_post_meta()` → registers `wpai_generated` post meta for attachment post type
- `wp_abilities_api_init` → registers the `ai/image-generation`, `ai/image-import`, and `ai/image-prompt-generation` abilities
- `admin_enqueue_scripts` → `enqueue_assets()` loads assets on `post.php` and `post-new.php` screens for post types that support featured images
- `enqueue_block_editor_assets` → `enqueue_inline_assets()` loads the same assets in the block editor for inline image generation
Expand Down Expand Up @@ -73,7 +73,7 @@ All three abilities can be called directly via REST API, making them useful for
- Updates the editor store to set the imported image as featured image
- Shows a loading state on the button and a progress message (with spinner) under the button while generating; clears both on success or error
- Handles error notifications via the notices store
- `AILabel` component displays a label for AI-generated images by checking the `ai_generated` meta
- `AILabel` component displays a label for AI-generated images by checking the `wpai_generated` meta

3. **React Side (Inline Image Generation):**
- `inline.tsx` registers two filters for supported blocks (`core/image`, `core/cover`, `core/media-text`, `core/gallery`):
Expand Down Expand Up @@ -103,7 +103,7 @@ All three abilities can be called directly via REST API, making them useful for
- Accepts base64 image data and metadata (filename, title, description, alt_text, mime_type, meta)
- Decodes base64 data and creates temporary file
- Uses WordPress `media_handle_sideload()` to import into media library
- Sets attachment metadata and custom meta (like `ai_generated`)
- Sets attachment metadata and custom meta (like `wpai_generated`)
- Returns attachment data (id, url, filename, title, description, alt_text)

### Input Schemas
Expand Down Expand Up @@ -837,7 +837,7 @@ npm run test:php

### Image Metadata

- Imported images are marked with `ai_generated` post meta (set to `1`)
- Imported images are marked with `wpai_generated` post meta (set to `1`)
- This meta is registered for the `attachment` post type and is available in REST API
- The `AILabel` component checks this meta to display the AI-generated label
- Additional custom meta can be passed via the `meta` parameter in the import ability
Expand Down
2 changes: 1 addition & 1 deletion includes/Abilities/Image/Import_Base64_Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ protected function import_image( string $data, array $args = array() ) {
);

if ( $args['ai_generated'] ) {
$post_data['meta_input']['ai_generated'] = 1;
$post_data['meta_input']['wpai_generated'] = 1;
}

$attachment_id = media_handle_sideload( $file_array, 0, $args['description'], $post_data );
Expand Down
2 changes: 2 additions & 0 deletions includes/Admin/Upgrades.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use WordPress\AI\Admin\Upgrades\V0_5_0;
use WordPress\AI\Admin\Upgrades\V0_6_0;
use WordPress\AI\Admin\Upgrades\V1_0_0;
use WordPress\AI\Admin\Upgrades\V1_3_0;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
Expand Down Expand Up @@ -52,6 +53,7 @@ final class Upgrades {
V0_5_0::class,
V0_6_0::class,
V1_0_0::class,
V1_3_0::class,
);

/**
Expand Down
121 changes: 121 additions & 0 deletions includes/Admin/Upgrades/V1_3_0.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php
/**
* Upgrade routines for version 1.3.0.
*
* @package WordPress\AI\Admin\Upgrades
* @since x.x.x
*/

declare( strict_types=1 );

namespace WordPress\AI\Admin\Upgrades;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
* Upgrade routine for standardizing meta keys on the `wpai_` prefix.
*
* Renames the legacy `ai_generated` and `ai_generated_summary` post meta keys and
* the `ai_note` comment meta key to their `wpai_`-prefixed equivalents so all
* plugin-owned meta shares a consistent namespace.
*
* @since x.x.x
* @internal
*/
class V1_3_0 extends Abstract_Upgrade {
Comment thread
dkotter marked this conversation as resolved.

/**
* {@inheritDoc}
*
* @since x.x.x
*/
public static string $version = '1.3.0';

/**
* {@inheritDoc}
*
* Migrates post and comment meta keys from the legacy `ai_` prefix to the
* `wpai_` prefix.
*
* @since x.x.x
*/
protected function upgrade(): void {
$this->rename_post_meta_key( 'ai_generated', 'wpai_generated' );
$this->rename_post_meta_key( 'ai_generated_summary', 'wpai_generated_summary' );
$this->rename_comment_meta_key( 'ai_note', 'wpai_note' );
Comment on lines +44 to +46

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.

Since we're using direct DB queries to update meta, may need a cache flush after this runs in order to ensure any cached values are flushed out. Likely not a problem as we're not changing data, just keys but may be better safe than sorry

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, Will flush the cache to ensure everything works expected.

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.

wp_cache_flush works and is likely fine since this is a one-time thing but does clear out the entire cache, whereas we only need to worry about the post and comment cache. We could modify things to track which post IDs and comment IDs were updated and then just clear those caches (potentially using clean_post_cache/clean_comment_cache). Any thoughts on that or is that not needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I do not think, clean_post_cache/clean_comment_cache is needed, as wp_cache_flush would wipe every cache on the site. So IMO it's not needed as post/comment and every object cache would be flushed.

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.

Yes, wp_cache_flush will clear the cache we need here but also deletes all other caches. So on sites heavily reliant on cached data for performance, running wp_cache_flush can have a negative impact on the site. Likely won't be a problem here but we could be more specific with the cache flush functions to only clear what we need

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, also this is a one off thing, so we can go with wp_cache_flush here.


// Cache flush to update any stale data.
wp_cache_flush();
}

/**
* Renames a post meta key for every row that uses it.
*
* @since x.x.x
*
* @param string $old_key The existing meta key.
* @param string $new_key The meta key to migrate to.
*/
private function rename_post_meta_key( string $old_key, string $new_key ): void {
global $wpdb;

// Rename the old key to the new key, but only for posts that don't already
// have the new key set. This avoids creating duplicate meta if new data was
// written under the new key before this migration ran.
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare(
"UPDATE {$wpdb->postmeta} AS pm
LEFT JOIN {$wpdb->postmeta} AS existing
ON existing.post_id = pm.post_id AND existing.meta_key = %s
SET pm.meta_key = %s
WHERE pm.meta_key = %s AND existing.meta_id IS NULL",
$new_key,
$new_key,
$old_key
)
);

// Any rows still using the old key are duplicates of a pre-existing new key.
// The new value is authoritative, so remove the redundant old rows.
$wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->postmeta,
array( 'meta_key' => $old_key )
);
}

/**
* Renames a comment meta key for every row that uses it.
*
* @since x.x.x
*
* @param string $old_key The existing meta key.
* @param string $new_key The meta key to migrate to.
*/
private function rename_comment_meta_key( string $old_key, string $new_key ): void {
global $wpdb;

// Rename the old key to the new key, but only for comments that don't already
// have the new key set. This avoids creating duplicate meta if new data was
// written under the new key before this migration ran.
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare(
"UPDATE {$wpdb->commentmeta} AS cm
LEFT JOIN {$wpdb->commentmeta} AS existing
ON existing.comment_id = cm.comment_id AND existing.meta_key = %s
SET cm.meta_key = %s
WHERE cm.meta_key = %s AND existing.meta_id IS NULL",
$new_key,
$new_key,
$old_key
)
);

// Any rows still using the old key are duplicates of a pre-existing new key.
// The new value is authoritative, so remove the redundant old rows.
$wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->commentmeta,
array( 'meta_key' => $old_key )
);
}
}
6 changes: 3 additions & 3 deletions includes/Experiments/Editorial_Notes/Editorial_Notes.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function register(): void {

register_meta(
'comment',
'ai_note',
'wpai_note',

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.

Something I hadn't thought about until now, these meta keys we're renaming are set as show_in_rest => true. This means this data is exposed via REST and there could be others that are consuming/using that data. This rename would break their integrations.

I would guess there's a low (potentially zero) amount of users doing this but any thoughts on what is a breaking change here? At the very least, we'll need to call this out as a breaking change in our changelog but wondering if there's anything else we should consider here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would guess there's a low (potentially zero) amount of users doing this but any thoughts on what is a breaking change here?

Yes, there could be breaking change, for eg; Users using/depend on Rest API for data would break, since ai_generated key would no longer be return in response, it would be wpai_generated. So user who access it as meta.ai_generated would get undefined, so it would silently break that part.

Another example could be user external writes request with body params as {"meta": {"ai_generated": ""}} would break, because rest does not knows about this key, so it would silently drops this key. So until noticed it won't be visible.

May be we have 2 pathways here,

  1. Document it as a breaking change in newer version. Simplest but may have backward compatibility issue. But since amount of users would potentially be zero doing this would work.
  2. Keep the meta key and register newer one side by side, drop the other one after few releases and add the deprecation notice or similar using the older keys could be an option.

IMO, we should go with the 1st, just document it as a breaking change in the release doc.

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.

Yeah, I think I'm fine with option 1 here. But probably worth us discussing @jeffpaul prior to merging this in

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @dkotter @jeffpaul, Any updates on this? Let's get this updated before more users uses the AI plugin, so better to update these inconsistencies.

Thanks,

@dkotter dkotter Aug 5, 2026

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.

I've not had a chance to discuss with Jeff but I'm still fine with option 1 here so let's move forward with that

Edit: just saw Jeff left a comment on the Issue. I'm fine with the renaming

@hbhalodia hbhalodia Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, @dkotter I think this would be added at the time of release right? So no any changes in PR.

Let me know if there is any other feedback on this?

array(
'type' => 'boolean',
'single' => true,
Expand Down Expand Up @@ -97,7 +97,7 @@ public function register_abilities(): void {
* Overrides the author fields for AI-generated Notes before they are inserted.
*
* Fires via the rest_pre_insert_comment filter. When the REST request includes
* meta.ai_note = true on a Note (comment_type "note") created by a user who can
* meta.wpai_note = true on a Note (comment_type "note") created by a user who can
* edit the target post, replaces the authenticated user's identity with a generic
* "AI" author so Notes are not attributed to a personal account.
*
Expand All @@ -114,7 +114,7 @@ public function maybe_set_ai_author( $prepared_comment, \WP_REST_Request $reques

$meta = $request->get_param( 'meta' );

if ( ! is_array( $meta ) || empty( $meta['ai_note'] ) ) {
if ( ! is_array( $meta ) || empty( $meta['wpai_note'] ) ) {
return $prepared_comment;
}

Expand Down
2 changes: 1 addition & 1 deletion includes/Experiments/Summarization/Summarization.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function register_bulk_action_hooks_for_screen(): void {
public function register_post_meta(): void {
register_meta(
'post',
'ai_generated_summary',
'wpai_generated_summary',
array(
'type' => 'string',
'single' => true,
Expand Down
2 changes: 1 addition & 1 deletion includes/Features/Image_Generation/Image_Generation.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public function inject_generate_image_button(): void {
public function register_post_meta(): void {
register_post_meta(
'attachment',
'ai_generated',
'wpai_generated',
array(
'type' => 'integer',
'single' => true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ async function createNote(
type: 'note',
status: 'hold',
parent: existingNoteId ?? 0,
meta: { ai_note: true },
meta: { wpai_note: true },
}
) ) as NoteRecord | undefined;

Expand Down
2 changes: 1 addition & 1 deletion src/experiments/summarization/bulk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ async function processBulkSummary(): Promise< void > {
method: 'POST',
data: {
content: newContent,
meta: { ai_generated_summary: summary },
meta: { wpai_generated_summary: summary },
},
} );
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export function useSummaryGeneration() {
editPost( {
meta: {
...meta,
ai_generated_summary: generatedSummary,
wpai_generated_summary: generatedSummary,
},
} );

Expand Down
2 changes: 1 addition & 1 deletion src/features/image-generation/components/AILabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function AILabel( { label }: AILabelProps ): React.JSX.Element {

return (
<>
{ image && image?.meta?.ai_generated === 1 && (
{ image && image?.meta?.wpai_generated === 1 && (
<div className="ai-label">
<span
className="ai-label__text"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ public function test_execute_callback_with_custom_metadata() {
$this->assertEquals( 'Custom Test Image Alt Text', get_post_meta( $result['image']['id'], '_wp_attachment_image_alt', true ), 'Attachment alt text should match' );

// Verify the AI-generated flag was saved.
$this->assertSame( '1', get_post_meta( $result['image']['id'], 'ai_generated', true ), 'AI-generated flag should be saved' );
$this->assertSame( '1', get_post_meta( $result['image']['id'], 'wpai_generated', true ), 'AI-generated flag should be saved' );
}

/**
Expand Down
Loading
Loading