Skip to content
wiki.fftac.org

Conversationrepository - Source Excerpt 01

Back to Conversationrepository

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/spiralist-workspace/includes/Domain/ConversationRepository.php so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/plugins/spiralist-workspace/includes/Domain/ConversationRepository.php

<?php

namespace SpiralistWorkspace\Domain;

use SpiralistWorkspace\Installer;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Persists prompt-based conversations and messages.
 */
class ConversationRepository
{
    /**
     * Creates a conversation row.
     */
    public function create(array $data): int
    {
        global $wpdb;

        $now = gmdate('Y-m-d H:i:s');

        $wpdb->insert(
            Installer::table('conversations'),
            [
                'prompt_post_id' => (int) ($data['prompt_post_id'] ?? 0),
                'owner_user_id' => (int) ($data['owner_user_id'] ?? 0),
                'title' => sanitize_text_field((string) ($data['title'] ?? 'Untitled Conversation')),
                'visibility' => sanitize_key((string) ($data['visibility'] ?? 'private')),
                'created_utc' => $now,
                'updated_utc' => $now,
                'archived_utc' => null,
                'openai_reference' => !empty($data['openai_reference']) ? sanitize_text_field((string) $data['openai_reference']) : null,
                'conversation_settings_json' => wp_json_encode((array) ($data['conversation_settings_json'] ?? []), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            ],
            ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']
        );

        return (int) $wpdb->insert_id;
    }

    /**
     * Updates the latest runtime reference and timestamp.
     */
    public function update_openai_reference(int $conversation_id, ?string $reference): void
    {
        global $wpdb;

        $reference = trim((string) $reference);

        $wpdb->update(
            Installer::table('conversations'),
            [
                'openai_reference' => $reference !== '' ? sanitize_text_field($reference) : null,
                'updated_utc' => gmdate('Y-m-d H:i:s'),
            ],
            [
                'id' => $conversation_id,
            ],
            ['%s', '%s'],
            ['%d']
        );
    }

    /**
     * Decodes the stored conversation settings.
     */
    public function decode_settings(array $conversation): array
    {
        $decoded = json_decode((string) ($conversation['conversation_settings_json'] ?? ''), true);

        return is_array($decoded) ? $decoded : [];
    }

    /**
     * Updates conversation settings and timestamp.
     */
    public function update_settings(int $conversation_id, array $settings): void
    {
        global $wpdb;

        $wpdb->update(
            Installer::table('conversations'),
            [
                'conversation_settings_json' => wp_json_encode($settings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                'updated_utc' => gmdate('Y-m-d H:i:s'),
            ],
            [
                'id' => $conversation_id,
            ],
            ['%s', '%s'],
            ['%d']
        );
    }

    /**
     * Returns the active memory size and checkpoint state for a conversation.
     */
    public function memory_status(array $conversation, array $messages, int $threshold_tokens): array
    {
        $settings = $this->decode_settings($conversation);
        $memory_summary = trim((string) ($settings['memory_summary'] ?? ''));
        $memory_messages = $this->messages_for_memory($messages);
        $total_messages = count($memory_messages);
        $checkpoint_message_count = min(
            $total_messages,
            max(0, (int) ($settings['memory_summary_message_count'] ?? 0))
        );
        $active_messages = array_slice($memory_messages, $checkpoint_message_count);
        $threshold_tokens = max(1, $threshold_tokens);
        $estimated_active_tokens = $this->estimate_messages_tokens($active_messages) + $this->estimate_text_tokens($memory_summary);
        $estimated_total_tokens = $this->estimate_messages_tokens($memory_messages) + $this->estimate_text_tokens($memory_summary);
        $percent = (int) round(($estimated_active_tokens / $threshold_tokens) * 100);

        return [
            'threshold_tokens' => $threshold_tokens,
            'estimated_active_tokens' => $estimated_active_tokens,
            'estimated_total_tokens' => $estimated_total_tokens,
            'percent_of_threshold' => max(0, min(200, $percent)),
            'warning_level' => $estimated_active_tokens >= $threshold_tokens
                ? 'checkpoint'
                : ($estimated_active_tokens >= (int) floor($threshold_tokens * 0.8) ? 'soon' : 'ok'),
            'needs_checkpoint' => $estimated_active_tokens >= $threshold_tokens,
            'has_memory_summary' => $memory_summary !== '',
            'memory_summary_updated_utc' => (string) ($settings['memory_summary_updated_utc'] ?? ''),
            'memory_summary_message_count' => $checkpoint_message_count,
            'memory_summary_run_id' => (int) ($settings['memory_summary_run_id'] ?? 0),
            'total_messages' => $total_messages,
            'messages_since_summary' => count($active_messages),
        ];
    }

    /**
     * Keeps user-visible turns in the memory budget.
     */
    public function messages_for_memory(array $messages): array
    {
        return array_values(
            array_filter(
                $messages,
                static function ($message): bool {
                    if (!is_array($message)) {
                        return false;
                    }

                    $role = sanitize_key((string) ($message['role'] ?? ''));

                    return in_array($role, ['user', 'assistant'], true)
                        && trim((string) ($message['content'] ?? '')) !== '';
                }
            )
        );
    }

    /**
     * Estimates token pressure with a conservative character-based heuristic.
     */
    public function estimate_messages_tokens(array $messages): int
    {
        $characters = 0;

        foreach ($this->messages_for_memory($messages) as $message) {
            $characters += strlen((string) ($message['role'] ?? ''));
            $characters += strlen(wp_strip_all_tags((string) ($message['content'] ?? '')));
            $characters += 8;
        }

        return (int) ceil($characters / 4);
    }

    /**
     * Estimates tokens for a single text value.
     */
    public function estimate_text_tokens(string $text): int
    {
        $text = trim(wp_strip_all_tags($text));

        return $text === '' ? 0 : (int) ceil(strlen($text) / 4);
    }

    /**
     * Adds a message to a conversation.
     */
    public function add_message(int $conversation_id, string $role, string $content, ?string $openai_reference = null, array $metadata = []): int
    {
        global $wpdb;

        $wpdb->insert(
            Installer::table('conversation_messages'),
            [
                'conversation_id' => $conversation_id,
                'role' => sanitize_key($role),
                'content' => wp_kses_post($content),
                'created_utc' => gmdate('Y-m-d H:i:s'),
                'openai_reference' => $openai_reference ? sanitize_text_field($openai_reference) : null,
                'metadata_json' => !empty($metadata) ? wp_json_encode($metadata, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null,
            ],
            ['%d', '%s', '%s', '%s', '%s', '%s']
        );

        $this->touch($conversation_id);

        return (int) $wpdb->insert_id;
    }

    /**
     * Retrieves a conversation.
     */
    public function get(int $conversation_id): array
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT * FROM ' . Installer::table('conversations') . ' WHERE id = %d LIMIT 1',
            $conversation_id
        );

        $row = $wpdb->get_row($sql, ARRAY_A);

        return is_array($row) ? $row : [];
    }

    /**
     * Lists conversations for a user.
     */
    public function list_for_user(int $user_id, int $limit = 20): array
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT * FROM ' . Installer::table('conversations') . ' WHERE owner_user_id = %d ORDER BY updated_utc DESC LIMIT %d',
            $user_id,
            max(1, $limit)
        );

        $rows = $wpdb->get_results($sql, ARRAY_A);

        return is_array($rows) ? $rows : [];
    }

    /**
     * Lists messages for a conversation.
     */
    public function get_messages(int $conversation_id): array
    {
        global $wpdb;

        $sql = $wpdb->prepare(
            'SELECT * FROM ' . Installer::table('conversation_messages') . ' WHERE conversation_id = %d ORDER BY created_utc ASC',
            $conversation_id
        );

        $rows = $wpdb->get_results($sql, ARRAY_A);

        return is_array($rows) ? $rows : [];
    }

    /**