0 && $chunk >= $max_chunks ) { \WP_CLI::warning( sprintf( 'Stopped after %d chunks (--max-chunks). Dirty: %d, push keys: %d. Re-run `wp aieo semantic onboard` to resume.', $chunk, self::dirty_count(), self::push_key_count() ) ); return; } $chunk++; $r = \WP_CLI::runcommand( 'aieo semantic onboard_step', array( 'launch' => true, 'return' => 'all', 'exit_error' => false, ) ); $step = self::parse_step_output( is_object( $r ) ? (string) $r->stdout : '' ); if ( null === $step ) { $parse_fails++; $stderr = is_object( $r ) ? trim( (string) $r->stderr ) : ''; \WP_CLI::warning( 'onboard_step returned no parseable status' . ( '' !== $stderr ? ' — stderr: ' . $stderr : '' ) ); if ( $parse_fails >= 3 ) { \WP_CLI::error( 'Three unparseable child responses in a row — aborting. The run state is intact; re-run to resume.' ); } sleep( 3 ); continue; } $parse_fails = 0; $status = (string) ( $step['status'] ?? '' ); \WP_CLI::log( sprintf( 'chunk %d: %s — dirty %d, push keys %d', $chunk, $status, self::dirty_count(), self::push_key_count() ) ); if ( 'continue' === $status ) { continue; } if ( 'retry' === $status ) { $wait = max( 5, (int) ( $step['retry_in'] ?? 0 ) ); \WP_CLI::log( sprintf( 'Transient transport error — retrying in %ds.', $wait ) ); sleep( $wait ); continue; } if ( 'locked' === $status ) { sleep( 3 ); continue; } if ( 'done' === $status ) { \WP_CLI::success( sprintf( 'Onboarding finished after %d chunks. Dirty: %d, push keys: %d.', $chunk, self::dirty_count(), self::push_key_count() ) ); return; } \WP_CLI::error( sprintf( 'Run stopped (%s)%s after %d chunks. Dirty: %d, push keys: %d.', '' !== $status ? $status : 'unknown', '' !== self::stopped_reason() ? ' — ' . self::stopped_reason() : '', $chunk, self::dirty_count(), self::push_key_count() ) ); } } public function onboard_step( $args, $assoc_args ) { if ( ! class_exists( 'AIEO_Semantic_Backfill' ) || ! AIEO_Semantic_Backfill::engine_ready() ) { \WP_CLI::line( (string) wp_json_encode( array( 'status' => 'failed', 'retry_in' => 0, 'stream' => 'onboard', 'note' => 'engine_absent', ) ) ); return; } $r = AIEO_Semantic_Backfill::tick_once( array( 'onboard' ) ); \WP_CLI::line( (string) wp_json_encode( $r ) ); } private static function parse_step_output( $stdout ) { $lines = array_reverse( preg_split( '/\r?\n/', trim( $stdout ) ) ); foreach ( $lines as $line ) { $line = trim( $line ); if ( '' === $line || '{' !== $line[0] ) { continue; } $decoded = json_decode( $line, true ); if ( is_array( $decoded ) && isset( $decoded['status'] ) ) { return $decoded; } } return null; } private static function dirty_count() { if ( ! class_exists( 'AIEO_Semantic_Index_Store' ) || ! method_exists( 'AIEO_Semantic_Index_Store', 'object_counts' ) ) { return 0; } wp_cache_delete( 'sem_object_counts', 'aieo_dmm' ); $counts = AIEO_Semantic_Index_Store::object_counts(); return (int) ( $counts['dirty'] ?? 0 ); } private static function push_key_count() { wp_cache_delete( 'aieo_sem_push_queue', 'options' ); wp_cache_delete( 'notoptions', 'options' ); $q = get_option( 'aieo_sem_push_queue' ); return ( is_array( $q ) && isset( $q['keys'] ) && is_array( $q['keys'] ) ) ? count( $q['keys'] ) : 0; } private static function stopped_reason() { wp_cache_delete( 'aieo_sem_backfill_progress', 'options' ); wp_cache_delete( 'notoptions', 'options' ); $prog = get_option( 'aieo_sem_backfill_progress' ); return ( is_array( $prog ) && isset( $prog['onboard']['stopped_reason'] ) ) ? (string) $prog['onboard']['stopped_reason'] : ''; } } \WP_CLI::add_command( 'aieo semantic', 'AIEO_CLI_Semantic_Backfill' ); }