* @author Maximum Software Inc * @license GPL-3.0-only * @link https://agenistrator.com/ * * SPDX-FileCopyrightText: 2025-2026 Alex Ruban * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc * SPDX-License-Identifier: GPL-3.0-only */ namespace Agenistrator; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Thread class. */ class Thread { /** * Thread ID (primary key) * * @var int */ private $id; /** * User ID (owner of the thread) * * @var int */ private $user_id; /** * Thread title * * @var string */ private $title; /** * Constructor * * @param int $id The thread ID. * @param int $user_id The user ID. * @param string $title The thread title. * @since 1.0.0 */ public function __construct( $id, $user_id, $title ) { $this->id = (int) $id; $this->user_id = (int) $user_id; $this->title = $title; } /** * Get the thread ID * * @return int * @since 1.0.0 */ public function get_id() { return $this->id; } /** * Get the user ID * * @return int * @since 1.0.0 */ public function get_user_id() { return $this->user_id; } /** * Get the thread title * * @return string * @since 1.0.0 */ public function get_title() { return $this->title; } /** * Get the database table name * * @return string */ private function get_table() { return Thread_Manager::get_instance()->get_table(); } /** * Get imported tools for this thread * * @return array Array of imported tool IDs. * @since 1.0.0 */ public function get_imported_tools() { global $wpdb; $table_name = $this->get_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; caching not applicable; table name is a hardcoded string literal with a trusted prefix, not user input. $value = $wpdb->get_var( $wpdb->prepare( "SELECT imported_tools FROM {$table_name} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded $this->id ) ); if ( empty( $value ) ) { return array(); } $tools = json_decode( $value, true ); return is_array( $tools ) ? $tools : array(); } /** * Add imported tools to this thread * * @param array $tool_ids Array of tool IDs to add. * @return bool Success. * @since 1.0.0 */ public function add_imported_tools( $tool_ids ) { $current = $this->get_imported_tools(); $merged = array_values( array_unique( array_merge( $current, $tool_ids ) ) ); return $this->set_imported_tools( $merged ); } /** * Remove imported tools from this thread * * @param array $tool_ids Array of tool IDs to remove. * @return bool Success. * @since 1.0.0 */ public function remove_imported_tools( $tool_ids ) { $current = $this->get_imported_tools(); $filtered = array_values( array_diff( $current, $tool_ids ) ); return $this->set_imported_tools( $filtered ); } /** * Set imported tools for this thread * * @param array $tool_ids Array of tool IDs. * @return bool Success. */ private function set_imported_tools( $tool_ids ) { global $wpdb; $table_name = $this->get_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable. $result = $wpdb->update( $table_name, array( 'imported_tools' => wp_json_encode( $tool_ids ) ), array( 'id' => $this->id ), array( '%s' ), array( '%d' ) ); return false !== $result; } /** * Get content guard for this thread * * Returns existing guard or generates and stores a new one. * The guard persists for the thread lifetime to enable LLM token caching. * * @return string Content guard string. * @since 1.0.0 */ public function get_content_guard() { global $wpdb; $table_name = $this->get_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; caching not applicable; table name is a hardcoded string literal with a trusted prefix, not user input. $value = $wpdb->get_var( $wpdb->prepare( "SELECT content_guard FROM {$table_name} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded $this->id ) ); if ( null !== $value ) { return $value; } return $this->reset_content_guard(); } /** * Reset content guard for this thread * * Generates and stores a new content guard. Called when thread is cleared * to ensure the guard changes, preventing prompt injection attacks. * * @return string The new content guard. * @since 1.0.0 */ public function reset_content_guard() { global $wpdb; $table_name = $this->get_table(); $guard = Content_Guard::generate_guard(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable. $wpdb->update( $table_name, array( 'content_guard' => $guard ), array( 'id' => $this->id ), array( '%s' ), array( '%d' ) ); return $guard; } /** * Get loop state for this thread * * @return array|null Loop state array or null if no state exists. * @since 1.0.0 */ public function get_loop_state() { global $wpdb; $table_name = $this->get_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; caching not applicable; table name is a hardcoded string literal with a trusted prefix, not user input. $value = $wpdb->get_var( $wpdb->prepare( "SELECT loop_state FROM {$table_name} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded $this->id ) ); if ( null === $value || '' === $value ) { return null; } $state = json_decode( $value, true ); return is_array( $state ) ? $state : null; } /** * Set loop state for this thread * * @param array $state Loop state array (ttl, iteration, consecutive_empty_responses). * @return bool Success. * @since 1.0.0 */ public function set_loop_state( $state ) { global $wpdb; $table_name = $this->get_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable. $result = $wpdb->update( $table_name, array( 'loop_state' => wp_json_encode( $state ) ), array( 'id' => $this->id ), array( '%s' ), array( '%d' ) ); return false !== $result; } /** * Clear loop state for this thread * * @return bool Success. * @since 1.0.0 */ public function clear_loop_state() { global $wpdb; $table_name = $this->get_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable. $result = $wpdb->update( $table_name, array( 'loop_state' => null ), array( 'id' => $this->id ), array( '%s' ), array( '%d' ) ); return false !== $result; } /** * Clear thread state * * Resets imported_tools and loop_state, regenerates content_guard. * * @return bool Success. * @since 1.0.0 */ public function clear() { global $wpdb; $table_name = $this->get_table(); $guard = Content_Guard::generate_guard(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; caching not applicable. $result = $wpdb->update( $table_name, array( 'imported_tools' => null, 'loop_state' => null, 'content_guard' => $guard, ), array( 'id' => $this->id ), array( '%s', '%s', '%s' ), array( '%d' ) ); return false !== $result; } }