fix(schema): self-heal generation_log writes on half-migrated state

A cron-triggered generation can run right after a deploy, before any
admin_init migration pass — inserts carrying the new agent_role /
prompt_version columns would fail silently on the old schema and lose
the cost row. Two-sided heal: (1) log_api_call/log_image_generation
retry the insert without the v0.18.0 columns when it fails; (2) the
db-version check now also runs on cron requests (init + wp_doing_cron),
so the schema migrates on the first cron hit, not the first admin visit.

Agent-Session: pipeline-v2-phase1-engineer-20260612
This commit is contained in:
peptiderepo
2026-06-11 04:33:22 +00:00
parent ed8ec9425f
commit 1062af2815
3 changed files with 59 additions and 20 deletions

View File

@@ -95,6 +95,10 @@ behavior change** to the Economy (single-pass) and multi-step publish paths.
### Fixed ### Fixed
- ARCHITECTURE.md documented the multi-step edit stage as `edit`; the code has always - ARCHITECTURE.md documented the multi-step edit stage as `edit`; the code has always
written `polish`. Canonicalized to **`polish`** everywhere. written `polish`. Canonicalized to **`polish`** everywhere.
- Half-migrated-schema hardening: generation_log inserts retry without the new audit
columns when the columns are missing (a cron run right after deploy no longer loses cost
rows), and the db-version self-heal now also runs on cron requests (`init` +
`wp_doing_cron()`), not only on `admin_init`.
## [0.17.0] - 2026-06-11 ## [0.17.0] - 2026-06-11

View File

@@ -46,6 +46,10 @@ class PRAutoBlogger {
$this->ajax_handlers = new PRAutoBlogger_Ajax_Handlers( $this->executor->get_model_registry() ); $this->ajax_handlers = new PRAutoBlogger_Ajax_Handlers( $this->executor->get_model_registry() );
add_action( 'admin_init', array( $this, 'on_check_db_version' ) ); add_action( 'admin_init', array( $this, 'on_check_db_version' ) );
// v0.18.0: cron requests must also self-heal the schema — after a
// deploy, scheduled generation can run before any admin page load,
// and new-column inserts would silently fail on the old schema.
add_action( 'init', array( $this, 'on_check_db_version_for_cron' ) );
add_filter( 'cron_schedules', array( $this, 'filter_add_cron_schedules' ) ); add_filter( 'cron_schedules', array( $this, 'filter_add_cron_schedules' ) );
if ( is_admin() ) { if ( is_admin() ) {
@@ -306,6 +310,21 @@ class PRAutoBlogger {
} }
} }
/**
* Run the db-version check on cron requests (admin_init never fires
* there). Gated to wp_doing_cron() so normal frontend requests pay
* nothing beyond one autoloaded option read.
*
* Side effects: may run the same migrations as on_check_db_version().
*
* @return void
*/
public function on_check_db_version_for_cron(): void {
if ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) {
$this->on_check_db_version();
}
}
/** /**
* Add custom cron schedules (six-hourly for metrics collection). * Add custom cron schedules (six-hourly for metrics collection).
* *

View File

@@ -127,9 +127,19 @@ class PRAutoBlogger_Cost_Tracker {
$table = $wpdb->prefix . 'prautoblogger_generation_log'; $table = $wpdb->prefix . 'prautoblogger_generation_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->insert( $table, $log_entry->to_db_row() ); $inserted = $wpdb->insert( $table, $log_entry->to_db_row() );
if ( false === $wpdb->insert_id ) { // Self-healing on a half-migrated schema (e.g. cron fires after a
// deploy but before any admin_init migration pass): retry without
// the v0.18.0 audit columns rather than losing the cost row.
if ( false === $inserted ) {
$legacy_row = $log_entry->to_db_row();
unset( $legacy_row['agent_role'], $legacy_row['prompt_version'] );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$inserted = $wpdb->insert( $table, $legacy_row );
}
if ( false === $inserted || false === $wpdb->insert_id ) {
PRAutoBlogger_Logger::instance()->error( 'Failed to log API call: ' . $wpdb->last_error, 'cost-tracker' ); PRAutoBlogger_Logger::instance()->error( 'Failed to log API call: ' . $wpdb->last_error, 'cost-tracker' );
} }
} }
@@ -179,25 +189,31 @@ class PRAutoBlogger_Cost_Tracker {
} }
$table = $wpdb->prefix . 'prautoblogger_generation_log'; $table = $wpdb->prefix . 'prautoblogger_generation_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $row = array(
$wpdb->insert( 'post_id' => $post_id,
$table, 'run_id' => $this->run_id,
array( 'stage' => $stage,
'post_id' => $post_id, 'provider' => 'cloudflare-workers-ai',
'run_id' => $this->run_id, 'model' => $model,
'stage' => $stage, 'prompt_tokens' => 0,
'provider' => 'cloudflare-workers-ai', 'completion_tokens' => 0,
'model' => $model, 'estimated_cost' => $cost_usd,
'prompt_tokens' => 0, 'response_status' => 'success',
'completion_tokens' => 0, 'error_message' => '',
'estimated_cost' => $cost_usd, 'agent_role' => PRAutoBlogger_Stage_Display_Map::default_agent_role( $stage ),
'response_status' => 'success', 'prompt_version' => $this->resolve_prompt_version( $stage, null ),
'error_message' => '', 'created_at' => current_time( 'mysql' ),
'agent_role' => PRAutoBlogger_Stage_Display_Map::default_agent_role( $stage ),
'prompt_version' => $this->resolve_prompt_version( $stage, null ),
'created_at' => current_time( 'mysql' ),
)
); );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$inserted = $wpdb->insert( $table, $row );
// Self-healing on a half-migrated schema — see log_api_call().
if ( false === $inserted ) {
unset( $row['agent_role'], $row['prompt_version'] );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->insert( $table, $row );
}
} }
/** /**