<?php

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

class Niwa_AI_Admin_Autonomy_Session_State {
    const OPTION_KEY               = Niwa_AI_Constants::OPT_AUTONOMY_SESSION_STATE;
    const MAX_SESSIONS             = 24;
    const MAX_RECENT_TURNS         = 60;
    const MAX_RECENT_TURN_LOG_LINES = 10;
    const MAX_TOOL_EVENTS_PER_TURN = 60;
    const MAX_ACTIVE_TOOLS         = 48;
    const DEFAULT_ACTIVE_TOOL_TTL  = 1200;

    /** @var Niwa_AI_Auth_Provider_Interface|null */
    private $auth_manager;
    /** @var Niwa_AI_AI_Client_Interface|null */
    private $openai_client;
    private $model_router;
    private $workspace;
    private $self_improvement_manager;
    private $legacy_migration_checked = false;
    private $runtime_locks;

    public function __construct( ?Niwa_AI_Auth_Provider_Interface $auth_manager = null, ?Niwa_AI_AI_Client_Interface $openai_client = null, $model_router = null, $workspace = null, $self_improvement_manager = null ) {
        $this->auth_manager             = $auth_manager;
        $this->openai_client            = $openai_client;
        $this->model_router             = $model_router;
        $this->workspace                = $workspace;
        $this->self_improvement_manager = $self_improvement_manager;
        $this->runtime_locks             = new Niwa_AI_Admin_Autonomy_Runtime_Locks();

        // Surface a wp-admin notice if we're running on the legacy single-row
        // session backend on a site that has accumulated meaningful state.
        // The legacy backend stores ALL admin sessions in one big
        // OPTION_KEY row, which is a known hot-row for InnoDB contention
        // when more than one admin user runs Niwa concurrently. Workspace
        // backend (filesystem-per-session) eliminates that hazard entirely.
        if ( is_admin() ) {
            add_action( 'admin_notices', array( $this, 'maybe_render_legacy_backend_notice' ) );
        }
    }

    /**
     * Whether this instance is falling back to the legacy single-row
     * wp_options session backend. Public so other surfaces (settings UI,
     * diagnostics tool) can surface the same warning.
     *
     * @return bool
     */
    public function is_using_legacy_session_backend() {
        return ! $this->has_workspace_session_backend();
    }

    /**
     * Render a one-time admin notice when the plugin is running on the
     * legacy wp_options session backend. We require manage_options so
     * normal subscribers/customers never see this; we also dismiss when
     * the legacy state row is small/empty so brand-new installs don't get
     * a scary banner before they've even used the chat.
     *
     * @return void
     */
    public function maybe_render_legacy_backend_notice() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        if ( ! $this->is_using_legacy_session_backend() ) {
            return;
        }

        $legacy = $this->load_legacy_option_sessions();
        if ( ! is_array( $legacy ) || count( $legacy ) < 1 ) {
            return;
        }

        echo '<div class="notice notice-warning"><p><strong>Niwa AI:</strong> ';
        echo esc_html__(
            'Plugin trenutno koristi legacy session backend (sve admin sesije u jednom wp_options redu). To može da uzrokuje sporiji wp-admin kada više korisnika koristi Niwa istovremeno. Preporučujemo prebacivanje na workspace backend u Niwa AI → Personal Assistant podešavanjima.',
            'niwa-ai'
        );
        echo '</p></div>';
    }

    /**
     * Acquire a session lane lock atomically.
     *
     * Uses INSERT IGNORE + atomic UPDATE instead of the non-atomic
     * delete_option() + add_option() pattern to prevent race conditions.
     */
    public function acquire_session_lane( $session_id, $ttl = 180 ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return true;
        }

        return $this->runtime_locks->acquire( $this->session_lock_name( $session_id ), max( 30, intval( $ttl ) ) );
    }

    public function release_session_lane( $session_id ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return;
        }

        return $this->runtime_locks->release( $this->session_lock_name( $session_id ) );
    }

    /**
     * Refresh an existing session lane lock before it expires.
     *
     * This extends the TTL only when the lock is still active, which prevents
     * a long-running worker from silently losing its lane during slow Codex
     * turns while also avoiding accidental lock resurrection after expiry.
     *
     * @param string $session_id Session identifier.
     * @param int    $ttl        New TTL in seconds.
     * @return bool True when the lock was extended.
     */
    public function refresh_session_lane( $session_id, $ttl = 180 ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return false;
        }

        return $this->runtime_locks->refresh( $this->session_lock_name( $session_id ), max( 30, intval( $ttl ) ) );
    }

    /**
     * Non-blocking check whether a session lane lock is currently held.
     *
     * Session manifests still use lane locks for atomic file-backed writes.
     *
     * @param string $session_id Session identifier.
     * @return bool True if the lane is currently locked.
     */
    public function is_session_lane_locked( $session_id ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return false;
        }

        return $this->runtime_locks->is_locked( $this->session_lock_name( $session_id ) );
    }

    public function begin_turn( $session_id, $mode, $prompt, $meta = array() ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array(
                'id' => $this->generate_turn_id(),
            );
        }

		if ( 'subagent' !== sanitize_key( (string) $mode ) ) {
			$this->finish_dead_running_turns_when_actor_idle(
				$session_id,
				60,
				'cancelled',
				'Actor runtime is idle with no runner, lease, active message, or queue; previous running turn will not resume.'
			);
		}
        $this->finish_stale_running_turns( $session_id, 900, 'cancelled', 'Superseded by a later founder turn after the previous runtime stopped reporting.' );
        $session               = $this->get_session( $session_id );
        $session['updated_at'] = time();
        $session['mode']       = sanitize_key( (string) $mode );
        $session['turn_count'] = intval( isset( $session['turn_count'] ) ? $session['turn_count'] : 0 ) + 1;
        if ( ! empty( $meta['parent_session_id'] ) ) {
            $session['parent_session_id'] = sanitize_text_field( (string) $meta['parent_session_id'] );
        }
        if ( ! empty( $meta['parent_turn_id'] ) ) {
            $session['parent_turn_id'] = sanitize_text_field( (string) $meta['parent_turn_id'] );
        }
        if ( ! empty( $meta['subagent_label'] ) ) {
            $session['session_label'] = $this->truncate_text( (string) $meta['subagent_label'], 80 );
        }
        if ( ! empty( $meta['subagent_scope'] ) ) {
            $session['session_scope'] = sanitize_key( (string) $meta['subagent_scope'] );
        }
        if ( array_key_exists( 'subagent_depth', $meta ) ) {
            $session['subagent_depth'] = max( 0, intval( $meta['subagent_depth'] ) );
        }
        if ( array_key_exists( 'allow_writes', $meta ) ) {
            $session['allow_writes'] = ! empty( $meta['allow_writes'] );
        }
        $turn                  = array(
            'id'          => $this->generate_turn_id(),
            'mode'        => sanitize_key( (string) $mode ),
            'status'      => 'running',
            'prompt'      => $this->truncate_text( (string) $prompt, 280 ),
            'reply'       => '',
            'tool_events' => array(),
            'meta'        => $this->sanitize_meta( $meta ),
            'started_at'  => time(),
            'ended_at'    => 0,
        );
        $session['turns'][] = $turn;
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'    => 'turn_start',
                'turn_id' => $turn['id'],
                'mode'    => $turn['mode'],
                'prompt'  => (string) $prompt,
                'meta'    => $turn['meta'],
                'ts'      => time(),
            )
        );

        return $turn;
    }

    public function append_tool_event( $session_id, $turn_id, $tool_name, $args, $output, $meta = array() ) {
        $session_id = trim( (string) $session_id );
        $turn_id    = trim( (string) $turn_id );
        if ( '' === $session_id || '' === $turn_id ) {
            return;
        }

        $session = $this->get_session( $session_id );
        $index   = $this->find_turn_index( $session, $turn_id );
        if ( null === $index ) {
            return;
        }

        $tool_events   = isset( $session['turns'][ $index ]['tool_events'] ) && is_array( $session['turns'][ $index ]['tool_events'] ) ? $session['turns'][ $index ]['tool_events'] : array();
        $event         = array(
            'name'        => sanitize_key( (string) $tool_name ),
            'args'        => $this->truncate_text( $this->encode_compact_json( is_array( $args ) ? $args : array() ), 420 ),
            'result'      => $this->summarize_tool_output( $output ),
            'meta'        => $this->sanitize_meta( $meta ),
            'recorded_at' => time(),
        );
        $tool_events[] = $event;
        $session['turns'][ $index ]['tool_events'] = array_slice( $tool_events, -self::MAX_TOOL_EVENTS_PER_TURN );
        $session['updated_at']                     = time();
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'    => 'tool',
                'turn_id' => $turn_id,
                'tool'    => sanitize_key( (string) $tool_name ),
                'args'    => is_array( $args ) ? $args : array(),
                'output'  => $output,
                'meta'    => $this->sanitize_meta( $meta ),
                'ts'      => time(),
            )
        );
    }

    public function finish_turn( $session_id, $turn_id, $reply, $status = 'completed', $meta = array() ) {
        $session_id = trim( (string) $session_id );
        $turn_id    = trim( (string) $turn_id );
        if ( '' === $session_id || '' === $turn_id ) {
            return;
        }

        $session = $this->get_session( $session_id );
        $index   = $this->find_turn_index( $session, $turn_id );
        if ( null === $index ) {
            return;
        }

        $existing_meta                          = isset( $session['turns'][ $index ]['meta'] ) && is_array( $session['turns'][ $index ]['meta'] ) ? $session['turns'][ $index ]['meta'] : array();
        $merged_meta                            = array_merge( $existing_meta, $this->sanitize_meta( $meta ) );
        $session['turns'][ $index ]['status']   = sanitize_key( (string) $status );
        $session['turns'][ $index ]['reply']    = $this->truncate_text( (string) $reply, 640 );
        $session['turns'][ $index ]['meta']     = $merged_meta;
        $session['turns'][ $index ]['ended_at'] = time();
        $session['updated_at']                  = time();
        $session['last_turn_id']                = $turn_id;
        $session                                = $this->refresh_structured_session_ram( $session_id, $session, $session['turns'][ $index ] );
        $session                                = $this->compact_session_if_needed( $session_id, $session );
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'    => 'turn_end',
                'turn_id' => $turn_id,
                'status'  => sanitize_key( (string) $status ),
                'reply'   => (string) $reply,
                'meta'    => $merged_meta,
                'ts'      => time(),
            )
        );

        $fresh_session = $this->get_session( $session_id );
        $fresh_index   = $this->find_turn_index( $fresh_session, $turn_id );
        $fresh_turn    = null !== $fresh_index && isset( $fresh_session['turns'][ $fresh_index ] ) && is_array( $fresh_session['turns'][ $fresh_index ] )
            ? $fresh_session['turns'][ $fresh_index ]
            : array();
        if ( ! $this->is_internal_history_record( $fresh_turn ) ) {
            $this->maybe_capture_turn_learning( $session_id, $fresh_session, $fresh_turn );
        }
    }

    public function finish_stale_running_turns( $session_id, $max_age = 900, $status = 'cancelled', $reason = '' ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return 0;
        }

        $session = $this->get_session( $session_id );
        if ( empty( $session['turns'] ) || ! is_array( $session['turns'] ) ) {
            return 0;
        }

        $now      = time();
        $max_age  = max( 60, intval( $max_age ) );
        $status   = sanitize_key( (string) $status );
        $reason   = '' !== trim( (string) $reason ) ? trim( (string) $reason ) : 'Stale running turn closed by runtime state repair.';
        $finished = 0;

        foreach ( $session['turns'] as $index => $turn ) {
            if ( ! is_array( $turn ) ) {
                continue;
            }

            $turn_status = isset( $turn['status'] ) ? sanitize_key( (string) $turn['status'] ) : '';
            $started_at  = intval( isset( $turn['started_at'] ) ? $turn['started_at'] : 0 );
            $ended_at    = intval( isset( $turn['ended_at'] ) ? $turn['ended_at'] : 0 );

            if ( 'running' !== $turn_status || $ended_at > 0 || $started_at <= 0 || ( $now - $started_at ) < $max_age ) {
                continue;
            }

            $existing_meta = isset( $turn['meta'] ) && is_array( $turn['meta'] ) ? $turn['meta'] : array();
            $session['turns'][ $index ]['status']   = $status;
            $session['turns'][ $index ]['ended_at'] = $now;
            $session['turns'][ $index ]['meta']     = array_merge(
                $existing_meta,
                $this->sanitize_meta(
                    array(
                        'error'               => $reason,
                        'stale_runtime_close' => true,
                    )
                )
            );
            $finished++;
        }

        if ( $finished <= 0 ) {
            return 0;
        }

        $session['updated_at'] = $now;
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'     => 'runtime_state_repair',
                'status'   => $status,
                'reason'   => $reason,
                'count'    => $finished,
                'max_age'  => $max_age,
                'ts'       => $now,
            )
        );

        return $finished;
    }

    public function finish_dead_running_turns_when_actor_idle( $session_id, $max_age = 60, $status = 'cancelled', $reason = '' ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return 0;
        }

        $actor_state = $this->read_session_actor_state( $session_id );
        if ( ! $this->actor_state_is_idle( $actor_state ) ) {
            return 0;
        }

        return $this->finish_stale_running_turns(
            $session_id,
            $max_age,
            $status,
            '' !== trim( (string) $reason ) ? $reason : 'Actor runtime is idle; previous running turn will not resume.'
        );
    }

    public function finish_dead_running_turns_for_idle_actor_sessions( $max_age = 60, $status = 'cancelled', $reason = '' ) {
        $finished = 0;
        foreach ( $this->list_sessions( self::MAX_SESSIONS ) as $session ) {
            if ( ! is_array( $session ) || empty( $session['session_id'] ) ) {
                continue;
            }
            $has_running = false;
            foreach ( isset( $session['recent_turns'] ) ? (array) $session['recent_turns'] : array() as $turn ) {
                if ( is_array( $turn ) && 'running' === sanitize_key( isset( $turn['status'] ) ? (string) $turn['status'] : '' ) ) {
                    $has_running = true;
                    break;
                }
            }
            if ( ! $has_running ) {
                continue;
            }
            $finished += intval(
                $this->finish_dead_running_turns_when_actor_idle(
                    (string) $session['session_id'],
                    $max_age,
                    $status,
                    $reason
                )
            );
        }

        return $finished;
    }

    private function read_session_actor_state( $session_id ) {
        if (
            ! $this->workspace
            || ! method_exists( $this->workspace, 'get_session_actor_state_path' )
        ) {
            return array();
        }

        $path = (string) $this->workspace->get_session_actor_state_path( $session_id );
        if ( '' === $path || ! is_file( $path ) || ! is_readable( $path ) ) {
            return array();
        }

        $raw = file_get_contents( $path );
        if ( false === $raw || '' === trim( (string) $raw ) ) {
            return array();
        }

        $decoded = json_decode( (string) $raw, true );
        return is_array( $decoded ) ? $decoded : array();
    }

    private function actor_state_is_idle( $actor_state ) {
        $actor_state = is_array( $actor_state ) ? $actor_state : array();
        $now         = time();

        $runner_id = isset( $actor_state['runner_id'] ) ? trim( (string) $actor_state['runner_id'] ) : '';
        if ( '' !== $runner_id ) {
            return false;
        }

        $lease_expires_at = intval( isset( $actor_state['lease_expires_at'] ) ? $actor_state['lease_expires_at'] : 0 );
        if ( $lease_expires_at > $now ) {
            return false;
        }

        if ( ! empty( $actor_state['active_message'] ) ) {
            return false;
        }

        if ( ! empty( $actor_state['pending_messages'] ) ) {
            return false;
        }

        if ( ! empty( $actor_state['queue'] ) ) {
            return false;
        }

        return true;
    }

    public function record_subagent_result( $session_id, $parent_turn_id, $record ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return;
        }

        $session = $this->get_session( $session_id );
        $entry   = array(
            'id'         => isset( $record['id'] ) ? sanitize_text_field( (string) $record['id'] ) : $this->generate_turn_id(),
            'job_id'     => isset( $record['job_id'] ) ? sanitize_text_field( (string) $record['job_id'] ) : '',
            'label'      => $this->truncate_text( isset( $record['label'] ) ? (string) $record['label'] : 'subagent', 60 ),
            'task'       => $this->truncate_text( isset( $record['task'] ) ? (string) $record['task'] : '', 180 ),
            'status'     => sanitize_key( isset( $record['status'] ) ? (string) $record['status'] : 'completed' ),
            'summary'    => $this->truncate_text( isset( $record['summary'] ) ? (string) $record['summary'] : '', 400 ),
            'session_id' => isset( $record['session_id'] ) ? sanitize_text_field( (string) $record['session_id'] ) : '',
            'ts'         => time(),
        );

        $recent_subagents = isset( $session['recent_subagents'] ) && is_array( $session['recent_subagents'] ) ? $session['recent_subagents'] : array();
        $already_recorded = false;
        foreach ( $recent_subagents as $recent_index => $recent_entry ) {
            if ( '' !== $entry['job_id'] && is_array( $recent_entry ) && $entry['job_id'] === (string) ( $recent_entry['job_id'] ?? '' ) ) {
                $recent_subagents[ $recent_index ] = $entry;
                $already_recorded = true;
            }
        }
        if ( ! $already_recorded ) {
            $recent_subagents[] = $entry;
        }
        $session['recent_subagents'] = array_slice( $recent_subagents, -8 );

        $index = $this->find_turn_index( $session, (string) $parent_turn_id );
        if ( null !== $index ) {
            $meta = isset( $session['turns'][ $index ]['meta'] ) && is_array( $session['turns'][ $index ]['meta'] ) ? $session['turns'][ $index ]['meta'] : array();
            if ( empty( $meta['subagents'] ) || ! is_array( $meta['subagents'] ) ) {
                $meta['subagents'] = array();
            }
            $meta_entry_replaced = false;
            foreach ( $meta['subagents'] as $meta_index => $meta_entry ) {
                if ( '' !== $entry['job_id'] && is_array( $meta_entry ) && $entry['job_id'] === (string) ( $meta_entry['job_id'] ?? '' ) ) {
                    $meta['subagents'][ $meta_index ] = $entry;
                    $meta_entry_replaced = true;
                    $already_recorded    = true;
                }
            }
            if ( ! $meta_entry_replaced ) {
                $meta['subagents'][] = $entry;
            }
            $session['turns'][ $index ]['meta'] = $meta;
        }

        $session['updated_at'] = time();
        $this->save_session( $session_id, $session );
        if ( ! $already_recorded ) {
            $this->append_transcript_entry(
                $session_id,
                array(
                    'type'            => 'subagent_result',
                    'parent_turn_id'  => (string) $parent_turn_id,
                    'subagent_record' => $entry,
                    'ts'              => time(),
                )
            );
        }
    }

    public function get_context_blocks( $session_id ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array(
                'session_worklog_summary' => '',
                'session_recent_turn_log' => '',
                'session_subagent_notes'  => '',
                'session_notes'           => '',
                'session_product_context' => '',
                'session_active_tools'    => '',
                'session_extension_workflow' => '',
                'session_task_attempt_budget' => '',
            );
        }

        $session = $this->get_session( $session_id );

        return array(
            'session_worklog_summary' => isset( $session['worklog_summary'] ) ? (string) $session['worklog_summary'] : '',
            'session_recent_turn_log' => $this->build_recent_turn_log( $session ),
            'session_subagent_notes'  => $this->build_recent_subagent_notes( $session ),
            'session_notes'           => $this->build_sticky_notes_text( $session ),
            'session_product_context' => $this->build_recent_product_context_text( $session ),
            'session_active_tools'    => $this->build_active_tools_text( $session ),
            'session_extension_workflow' => $this->build_extension_workflow_context_text( $session ),
            'session_task_attempt_budget' => $this->build_task_attempt_budget_context_text( $session ),
            'session_current_topic'   => isset( $session['session_ram']['current_topic'] ) ? (string) $session['session_ram']['current_topic'] : '',
            'session_immediate_goals' => $this->build_session_ram_list_text( isset( $session['session_ram']['immediate_goals'] ) ? $session['session_ram']['immediate_goals'] : array() ),
            'session_recent_progress' => $this->build_session_ram_list_text( isset( $session['session_ram']['recent_progress'] ) ? $session['session_ram']['recent_progress'] : array() ),
            'session_next_steps'      => $this->build_session_ram_list_text( isset( $session['session_ram']['next_steps'] ) ? $session['session_ram']['next_steps'] : array() ),
            'session_user_state'      => isset( $session['session_ram']['user_state'] ) ? (string) $session['session_ram']['user_state'] : '',
            'session_restart_recap'   => isset( $session['session_ram']['restart_recap'] ) ? (string) $session['session_ram']['restart_recap'] : '',
        );
    }

    public function get_extension_workflow_state( $session_id, $prompt = '' ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array();
        }

        $session = $this->get_session( $session_id );
        $workflow = isset( $session['extension_workflow'] ) && is_array( $session['extension_workflow'] )
            ? $this->normalize_extension_workflow_state( $session['extension_workflow'] )
            : array();
        if ( empty( $workflow ) ) {
            return array();
        }

        $status = isset( $workflow['status'] ) ? sanitize_key( (string) $workflow['status'] ) : '';
        if ( in_array( $status, array( 'awaiting_approval', 'approved' ), true ) && $this->extension_workflow_has_later_turn( $session, $workflow ) ) {
            return array();
        }

        $hash = $this->hash_task_prompt( $prompt );
        if ( '' !== $hash && ! empty( $workflow['original_task_hash'] ) && ! hash_equals( (string) $workflow['original_task_hash'], $hash ) ) {
            return array();
        }

        return $workflow;
    }

    public function update_extension_workflow_state( $session_id, $updates ) {
        $session_id = trim( (string) $session_id );
        $updates    = is_array( $updates ) ? $updates : array();
        if ( '' === $session_id || empty( $updates ) ) {
            return array();
        }

        $session  = $this->get_session( $session_id );
        $current  = isset( $session['extension_workflow'] ) && is_array( $session['extension_workflow'] ) ? $session['extension_workflow'] : array();
        $workflow = $this->normalize_extension_workflow_state( array_merge( $current, $updates ) );
        if ( empty( $workflow['created_at'] ) ) {
            $workflow['created_at'] = time();
        }
        $workflow['updated_at']              = time();
        $session['extension_workflow']       = $workflow;
        $session['updated_at']               = time();
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'               => 'extension_workflow_update',
                'extension_workflow' => $workflow,
                'ts'                 => time(),
            )
        );

        return $workflow;
    }

    public function clear_extension_workflow_state( $session_id ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return false;
        }

        if ( $this->overwrite_workspace_session(
            $session_id,
            function( $session ) {
                unset( $session['extension_workflow'] );
                $session['updated_at'] = time();
                return $session;
            }
        ) ) {
            $this->append_transcript_entry(
                $session_id,
                array(
                    'type' => 'extension_workflow_clear',
                    'ts'   => time(),
                )
            );

            return true;
        }

        $session = $this->get_session( $session_id );
        unset( $session['extension_workflow'] );
        $session['updated_at'] = time();
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type' => 'extension_workflow_clear',
                'ts'   => time(),
            )
        );

        return true;
    }

    public function get_task_attempt_budget_state( $session_id, $prompt = '', $task_identity = '' ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array();
        }

        $session = $this->get_session( $session_id );
        $state   = isset( $session['task_attempt_budget'] ) && is_array( $session['task_attempt_budget'] )
            ? $this->normalize_task_attempt_budget_state( $session['task_attempt_budget'] )
            : array();
        if ( empty( $state ) ) {
            return array();
        }

        $identity    = $this->hash_task_prompt( '' !== trim( (string) $task_identity ) ? $task_identity : $prompt );
        $stored_hash = ! empty( $state['task_identity_hash'] ) ? (string) $state['task_identity_hash'] : (string) ( $state['original_task_hash'] ?? '' );
        if ( '' !== $identity && '' !== $stored_hash && ! hash_equals( $stored_hash, $identity ) ) {
            return array();
        }

        return $state;
    }

    public function update_task_attempt_budget_state( $session_id, $prompt, $updates, $task_identity = '' ) {
        $session_id = trim( (string) $session_id );
        $prompt     = trim( (string) $prompt );
        $updates    = is_array( $updates ) ? $updates : array();
        if ( '' === $session_id ) {
            return array();
        }

        $session = $this->get_session( $session_id );
        $current = isset( $session['task_attempt_budget'] ) && is_array( $session['task_attempt_budget'] ) ? $session['task_attempt_budget'] : array();
        $hash          = $this->hash_task_prompt( $prompt );
        $identity      = $this->hash_task_prompt( '' !== trim( (string) $task_identity ) ? $task_identity : $prompt );
        $current_state = $this->normalize_task_attempt_budget_state( $current );
        $stored_hash   = ! empty( $current_state['task_identity_hash'] )
            ? (string) $current_state['task_identity_hash']
            : (string) ( $current_state['original_task_hash'] ?? '' );
        if ( '' !== $identity && '' !== $stored_hash && ! hash_equals( $stored_hash, $identity ) ) {
            $current = array();
        }

        $merged = array_merge( $current, $updates );
        if ( '' !== $hash ) {
            $merged['original_task_hash'] = $hash;
            $merged['original_task']      = $prompt;
        }
        if ( '' !== $identity ) {
            $merged['task_identity_hash'] = $identity;
        }
        if ( empty( $merged['created_at'] ) ) {
            $merged['created_at'] = time();
        }
        $merged['updated_at']            = time();
        $state                           = $this->normalize_task_attempt_budget_state( $merged );
        $session['task_attempt_budget']  = $state;
        $session['updated_at']           = time();
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'                => 'task_attempt_budget_update',
                'task_attempt_budget' => $state,
                'ts'                  => time(),
                'meta'                => array(
                    'visibility' => 'internal',
                    'internal'   => true,
                ),
            )
        );

        return $state;
    }

    public function clear_task_attempt_budget_state( $session_id, $prompt = '', $task_identity = '' ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return false;
        }

        $hash = $this->hash_task_prompt( '' !== trim( (string) $task_identity ) ? $task_identity : $prompt );
        if ( $this->overwrite_workspace_session(
            $session_id,
            function( $session ) use ( $hash ) {
                $stored_hash = (string) ( $session['task_attempt_budget']['task_identity_hash'] ?? ( $session['task_attempt_budget']['original_task_hash'] ?? '' ) );
                if ( '' === $hash || '' === $stored_hash || hash_equals( $stored_hash, $hash ) ) {
                    unset( $session['task_attempt_budget'] );
                    $session['updated_at'] = time();
                }
                return $session;
            }
        ) ) {
            return true;
        }

        $session = $this->get_session( $session_id );
        $stored_hash = (string) ( $session['task_attempt_budget']['task_identity_hash'] ?? ( $session['task_attempt_budget']['original_task_hash'] ?? '' ) );
        if ( '' !== $hash && '' !== $stored_hash && ! hash_equals( $stored_hash, $hash ) ) {
            return false;
        }
        unset( $session['task_attempt_budget'] );
        $session['updated_at'] = time();
        $this->save_session( $session_id, $session );

        return true;
    }

    public function load_active_tools( $session_id, $tool_names, $ttl = self::DEFAULT_ACTIVE_TOOL_TTL, $turn_id = '' ) {
        $session_id = trim( (string) $session_id );
        $turn_id    = sanitize_text_field( trim( (string) $turn_id ) );
        if ( '' === $session_id ) {
            return array();
        }

        $names = $this->sanitize_tool_name_list( $tool_names );
        if ( empty( $names ) ) {
            return $this->get_active_tool_names( $session_id );
        }

        $session = $this->get_session( $session_id );
        $active  = isset( $session['active_tools'] ) && is_array( $session['active_tools'] ) ? $session['active_tools'] : array();
        $now     = time();
        $ttl     = max( 120, min( 4 * HOUR_IN_SECONDS, intval( $ttl ) > 0 ? intval( $ttl ) : self::DEFAULT_ACTIVE_TOOL_TTL ) );

        foreach ( $names as $name ) {
            $current = isset( $active[ $name ] ) && is_array( $active[ $name ] ) ? $active[ $name ] : array();
            $active[ $name ] = array(
                'loaded_at'    => ! empty( $current['loaded_at'] ) ? intval( $current['loaded_at'] ) : $now,
                'last_used_at' => $now,
                'expires_at'   => $now + $ttl,
                'turn_id'      => $turn_id,
            );
        }

        $session['active_tools'] = $this->normalize_active_tools_map( $active );
        $session['updated_at']   = $now;
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'  => 'active_tools_load',
                'tools' => array_values( $names ),
                'ttl'   => $ttl,
                'ts'    => $now,
            )
        );

        return array_keys( $session['active_tools'] );
    }

    public function unload_active_tools( $session_id, $tool_names = array(), $clear_all = false ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array();
        }

        if ( $clear_all && $this->overwrite_workspace_session(
            $session_id,
            function( $session ) {
                $session['active_tools'] = array();
                $session['updated_at']   = time();
                return $session;
            }
        ) ) {
            $this->append_transcript_entry(
                $session_id,
                array(
                    'type'      => 'active_tools_unload',
                    'tools'     => array( '*' ),
                    'clear_all' => true,
                    'ts'        => time(),
                )
            );

            return array();
        }

        $session = $this->get_session( $session_id );
        $active  = isset( $session['active_tools'] ) && is_array( $session['active_tools'] ) ? $session['active_tools'] : array();
        $names   = $this->sanitize_tool_name_list( $tool_names );

        if ( $clear_all ) {
            $active = array();
        } else {
            foreach ( $names as $name ) {
                unset( $active[ $name ] );
            }
        }

        $session['active_tools'] = $this->normalize_active_tools_map( $active );
        $session['updated_at']   = time();
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'      => 'active_tools_unload',
                'tools'     => $clear_all ? array( '*' ) : array_values( $names ),
                'clear_all' => ! empty( $clear_all ),
                'ts'        => time(),
            )
        );

        return array_keys( $session['active_tools'] );
    }

    public function touch_active_tools( $session_id, $tool_names, $ttl = self::DEFAULT_ACTIVE_TOOL_TTL, $turn_id = '' ) {
        $session_id = trim( (string) $session_id );
        $turn_id    = sanitize_text_field( trim( (string) $turn_id ) );
        if ( '' === $session_id ) {
            return array();
        }

        $names = $this->sanitize_tool_name_list( $tool_names );
        if ( empty( $names ) ) {
            return $this->get_active_tool_names( $session_id );
        }

        $session = $this->get_session( $session_id );
        $active  = isset( $session['active_tools'] ) && is_array( $session['active_tools'] ) ? $session['active_tools'] : array();
        $now     = time();
        $ttl     = max( 120, min( 4 * HOUR_IN_SECONDS, intval( $ttl ) > 0 ? intval( $ttl ) : self::DEFAULT_ACTIVE_TOOL_TTL ) );
        $dirty   = false;

        foreach ( $names as $name ) {
            if ( empty( $active[ $name ] ) || ! is_array( $active[ $name ] ) ) {
                continue;
            }
            $active_turn_id = isset( $active[ $name ]['turn_id'] ) ? trim( (string) $active[ $name ]['turn_id'] ) : '';
            if ( '' !== $turn_id && $turn_id !== $active_turn_id ) {
                continue;
            }
            $active[ $name ]['last_used_at'] = $now;
            $active[ $name ]['expires_at']   = $now + $ttl;
            $dirty = true;
        }

        if ( $dirty ) {
            $session['active_tools'] = $this->normalize_active_tools_map( $active );
            $session['updated_at']   = $now;
            $this->save_session( $session_id, $session );
        }

        return array_keys( isset( $session['active_tools'] ) ? (array) $session['active_tools'] : array() );
    }

    public function get_active_tool_names( $session_id ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array();
        }

        $session = $this->get_session( $session_id );
        return array_keys( isset( $session['active_tools'] ) && is_array( $session['active_tools'] ) ? $session['active_tools'] : array() );
    }

    public function get_turn_active_tool_names( $session_id, $turn_id ) {
        $session_id = trim( (string) $session_id );
        $turn_id    = sanitize_text_field( trim( (string) $turn_id ) );
        if ( '' === $session_id || '' === $turn_id ) {
            return array();
        }

        $session = $this->get_session( $session_id );
        $active  = isset( $session['active_tools'] ) && is_array( $session['active_tools'] ) ? $session['active_tools'] : array();
        $names   = array();
        foreach ( $active as $name => $state ) {
            $state = is_array( $state ) ? $state : array();
            if ( $turn_id !== trim( (string) ( isset( $state['turn_id'] ) ? $state['turn_id'] : '' ) ) ) {
                continue;
            }
            $name = sanitize_key( (string) $name );
            if ( '' !== $name ) {
                $names[] = $name;
            }
        }

        return array_values( array_unique( $names ) );
    }

    public function remember_product_context( $session_id, $record ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id || empty( $record ) || ! is_array( $record ) ) {
            return false;
        }

        $session = $this->get_session( $session_id );
        $recent  = isset( $session['product_context_records'] ) && is_array( $session['product_context_records'] ) ? $session['product_context_records'] : array();

        $normalized = array(
            'tool'       => isset( $record['tool'] ) ? sanitize_key( (string) $record['tool'] ) : '',
            'label'      => $this->truncate_text( isset( $record['label'] ) ? (string) $record['label'] : '', 120 ),
            'query'      => $this->truncate_text( isset( $record['query'] ) ? (string) $record['query'] : '', 160 ),
            'product_id' => isset( $record['product_id'] ) ? intval( $record['product_id'] ) : 0,
            'variation_ids' => array_values( array_filter( array_map( 'intval', isset( $record['variation_ids'] ) ? (array) $record['variation_ids'] : array() ) ) ),
            'candidates' => array(),
            'recorded_at' => time(),
        );

        foreach ( array_slice( isset( $record['candidates'] ) && is_array( $record['candidates'] ) ? $record['candidates'] : array(), 0, 5 ) as $candidate ) {
            $normalized['candidates'][] = array(
                'post_id'    => isset( $candidate['post_id'] ) ? intval( $candidate['post_id'] ) : 0,
                'post_title' => $this->truncate_text( isset( $candidate['post_title'] ) ? (string) $candidate['post_title'] : '', 120 ),
                'score'      => isset( $candidate['score'] ) ? round( floatval( $candidate['score'] ), 3 ) : 0,
            );
        }

        $recent[]                         = $normalized;
        $session['product_context_records'] = array_slice( $recent, -8 );
        $session['updated_at']            = time();
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'   => 'product_context',
                'record' => $normalized,
                'ts'     => time(),
            )
        );

        return true;
    }

    public function get_recent_product_context( $session_id ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array();
        }

        $session = $this->get_session( $session_id );
        return isset( $session['product_context_records'] ) && is_array( $session['product_context_records'] ) ? array_values( $session['product_context_records'] ) : array();
    }

    public function remember_media_context( $session_id, $record ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id || empty( $record ) || ! is_array( $record ) ) {
            return false;
        }

        $attachment_id = isset( $record['attachment_image_attachment_id'] ) ? intval( $record['attachment_image_attachment_id'] ) : 0;
        $attachment_ids = isset( $record['attachment_image_attachment_ids'] ) && is_array( $record['attachment_image_attachment_ids'] )
            ? array_values( array_filter( array_map( 'intval', $record['attachment_image_attachment_ids'] ) ) )
            : array();
        $image_url     = isset( $record['attachment_image_url'] ) ? esc_url_raw( (string) $record['attachment_image_url'] ) : '';
        $image_urls    = isset( $record['attachment_image_urls'] ) && is_array( $record['attachment_image_urls'] )
            ? array_values( array_filter( array_map( 'esc_url_raw', array_map( 'strval', $record['attachment_image_urls'] ) ) ) )
            : array();

        if ( $attachment_id <= 0 && empty( $attachment_ids ) && '' === $image_url && empty( $image_urls ) ) {
            return false;
        }

        if ( $attachment_id <= 0 && ! empty( $attachment_ids ) ) {
            $attachment_id = intval( $attachment_ids[0] );
        }

        if ( '' === $image_url && ! empty( $image_urls ) ) {
            $image_url = (string) $image_urls[0];
        }

        $normalized = array(
            'attachment_image_attachment_id' => $attachment_id,
            'attachment_image_attachment_ids' => array_slice( array_values( array_unique( $attachment_ids ) ), 0, 10 ),
            'attachment_image_url'           => $image_url,
            'attachment_image_urls'          => array_slice( $image_urls, 0, 5 ),
            'message_id'                     => isset( $record['message_id'] ) ? intval( $record['message_id'] ) : 0,
            'recorded_at'                    => time(),
        );

        $session                         = $this->get_session( $session_id );
        $recent                          = isset( $session['media_context_records'] ) && is_array( $session['media_context_records'] ) ? $session['media_context_records'] : array();
        $recent[]                        = $normalized;
        $session['media_context_records'] = array_slice( $recent, -6 );
        $session['updated_at']           = time();
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'   => 'media_context',
                'record' => $normalized,
                'ts'     => time(),
            )
        );

        return true;
    }

    public function get_recent_media_context( $session_id, $max_age_seconds = 1800 ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array();
        }

        $session = $this->get_session( $session_id );
        $records = isset( $session['media_context_records'] ) && is_array( $session['media_context_records'] ) ? array_values( $session['media_context_records'] ) : array();
        $cutoff  = time() - max( 60, intval( $max_age_seconds ) );

        for ( $i = count( $records ) - 1; $i >= 0; $i-- ) {
            $record = is_array( $records[ $i ] ) ? $records[ $i ] : array();
            if ( ! empty( $record['recorded_at'] ) && intval( $record['recorded_at'] ) < $cutoff ) {
                continue;
            }
            if ( empty( $record['attachment_image_attachment_id'] ) && empty( $record['attachment_image_attachment_ids'] ) && empty( $record['attachment_image_url'] ) && empty( $record['attachment_image_urls'] ) ) {
                continue;
            }
            return $record;
        }

        return array();
    }

    /**
     * Aggregate every image attachment recorded across the recent N media
     * context records (default last 30 min). Useful when the founder sent
     * images across multiple turns ("evo dve slike", then "ubaci ih u
     * galeriju") — single-record getter only returns the latest turn.
     *
     * Returns:
     *   [
     *     'attachment_ids' => [123, 124, 125, ...],   // de-duped, newest-first
     *     'image_urls'     => ['https://…', 'https://…', ...],
     *     'records'        => array of raw records (each has recorded_at, message_id),
     *   ]
     */
    public function get_recent_media_attachments( $session_id, $max_age_seconds = 1800, $limit = 12 ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array(
                'attachment_ids' => array(),
                'image_urls'     => array(),
                'records'        => array(),
            );
        }

        $session = $this->get_session( $session_id );
        $records = isset( $session['media_context_records'] ) && is_array( $session['media_context_records'] ) ? array_values( $session['media_context_records'] ) : array();
        $cutoff  = time() - max( 60, intval( $max_age_seconds ) );
        $limit   = max( 1, min( 30, intval( $limit ) ) );

        $attachment_ids = array();
        $image_urls     = array();
        $kept_records   = array();

        // Walk newest-first so the resulting list is sorted by recency.
        for ( $i = count( $records ) - 1; $i >= 0; $i-- ) {
            $record = is_array( $records[ $i ] ) ? $records[ $i ] : array();
            if ( ! empty( $record['recorded_at'] ) && intval( $record['recorded_at'] ) < $cutoff ) {
                continue;
            }

            $rec_ids = isset( $record['attachment_image_attachment_ids'] ) && is_array( $record['attachment_image_attachment_ids'] )
                ? array_values( array_filter( array_map( 'intval', $record['attachment_image_attachment_ids'] ) ) )
                : array();
            if ( empty( $rec_ids ) && ! empty( $record['attachment_image_attachment_id'] ) ) {
                $rec_ids = array( intval( $record['attachment_image_attachment_id'] ) );
            }

            $rec_urls = isset( $record['attachment_image_urls'] ) && is_array( $record['attachment_image_urls'] )
                ? array_values( array_filter( array_map( 'esc_url_raw', array_map( 'strval', $record['attachment_image_urls'] ) ) ) )
                : array();
            if ( empty( $rec_urls ) && ! empty( $record['attachment_image_url'] ) ) {
                $rec_urls = array( esc_url_raw( (string) $record['attachment_image_url'] ) );
            }

            if ( empty( $rec_ids ) && empty( $rec_urls ) ) {
                continue;
            }

            $attachment_ids = array_merge( $attachment_ids, $rec_ids );
            $image_urls     = array_merge( $image_urls, $rec_urls );
            $kept_records[] = $record;

            if ( count( $attachment_ids ) >= $limit ) {
                break;
            }
        }

        $attachment_ids = array_values( array_unique( array_filter( array_map( 'intval', $attachment_ids ) ) ) );
        $image_urls     = array_values( array_unique( array_filter( $image_urls ) ) );

        return array(
            'attachment_ids' => array_slice( $attachment_ids, 0, $limit ),
            'image_urls'     => array_slice( $image_urls, 0, $limit ),
            'records'        => $kept_records,
        );
    }

    public function get_recent_work_notes( $session_id ) {
        $blocks = $this->get_context_blocks( $session_id );
        $parts  = array_filter(
            array(
                isset( $blocks['session_worklog_summary'] ) ? $blocks['session_worklog_summary'] : '',
                isset( $blocks['session_recent_turn_log'] ) ? $blocks['session_recent_turn_log'] : '',
            )
        );

        return implode( "\n", $parts );
    }

    public function get_session_snapshot( $session_id ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array();
        }

        $session = $this->get_session( $session_id );
        return array(
            'session_id'         => $session_id,
            'mode'               => isset( $session['mode'] ) ? (string) $session['mode'] : 'main',
            'updated_at'         => intval( isset( $session['updated_at'] ) ? $session['updated_at'] : 0 ),
            'turn_count'         => intval( isset( $session['turn_count'] ) ? $session['turn_count'] : count( isset( $session['turns'] ) ? (array) $session['turns'] : array() ) ),
            'compaction_count'   => intval( isset( $session['compaction_count'] ) ? $session['compaction_count'] : 0 ),
            'session_label'      => isset( $session['session_label'] ) ? (string) $session['session_label'] : '',
            'session_scope'      => isset( $session['session_scope'] ) ? (string) $session['session_scope'] : '',
            'subagent_depth'     => max( 0, intval( isset( $session['subagent_depth'] ) ? $session['subagent_depth'] : 0 ) ),
            'allow_writes'       => ! empty( $session['allow_writes'] ),
            'parent_session_id'  => isset( $session['parent_session_id'] ) ? (string) $session['parent_session_id'] : '',
            'parent_turn_id'     => isset( $session['parent_turn_id'] ) ? (string) $session['parent_turn_id'] : '',
            'last_turn_id'       => isset( $session['last_turn_id'] ) ? (string) $session['last_turn_id'] : '',
            'session_file'       => isset( $session['session_file'] ) ? (string) $session['session_file'] : $this->get_transcript_path( $session_id ),
            'worklog_summary'    => isset( $session['worklog_summary'] ) ? (string) $session['worklog_summary'] : '',
            'session_ram'        => isset( $session['session_ram'] ) && is_array( $session['session_ram'] ) ? $session['session_ram'] : $this->get_default_session_ram(),
            'recent_turns'       => isset( $session['turns'] ) ? array_values( (array) $session['turns'] ) : array(),
            'recent_subagents'   => isset( $session['recent_subagents'] ) ? array_values( (array) $session['recent_subagents'] ) : array(),
            'sticky_notes'       => isset( $session['sticky_notes'] ) ? array_values( (array) $session['sticky_notes'] ) : array(),
            'active_tools'       => array_keys( isset( $session['active_tools'] ) && is_array( $session['active_tools'] ) ? $session['active_tools'] : array() ),
            'extension_workflow' => isset( $session['extension_workflow'] ) && is_array( $session['extension_workflow'] ) ? $this->normalize_extension_workflow_state( $session['extension_workflow'] ) : array(),
            'task_attempt_budget' => isset( $session['task_attempt_budget'] ) && is_array( $session['task_attempt_budget'] ) ? $this->normalize_task_attempt_budget_state( $session['task_attempt_budget'] ) : array(),
            'transcript_excerpt' => $this->get_session_transcript_excerpt( $session_id, 16 ),
        );
    }

    public function list_sessions( $limit = 20, $session_prefix = '' ) {
        $sessions = $this->get_all_sessions();
        $records  = array();
        foreach ( $sessions as $session_id => $session ) {
            if ( '' !== $session_prefix && 0 !== strpos( (string) $session_id, (string) $session_prefix ) ) {
                continue;
            }
            $records[] = $this->get_session_snapshot( (string) $session_id );
        }

        usort(
            $records,
            function ( $a, $b ) {
                return intval( isset( $b['updated_at'] ) ? $b['updated_at'] : 0 ) <=> intval( isset( $a['updated_at'] ) ? $a['updated_at'] : 0 );
            }
        );

        return array_slice( $records, 0, max( 1, min( 50, intval( $limit ) ) ) );
    }

    public function add_sticky_note( $session_id, $text, $source = 'founder' ) {
        $session_id = trim( (string) $session_id );
        $text       = trim( (string) $text );
        if ( '' === $session_id || '' === $text ) {
            return false;
        }

        $session = $this->get_session( $session_id );
        $notes   = isset( $session['sticky_notes'] ) && is_array( $session['sticky_notes'] ) ? $session['sticky_notes'] : array();
        $notes[] = array(
            'text'        => $this->truncate_text( $text, 320 ),
            'source'      => sanitize_key( (string) $source ),
            'recorded_at' => time(),
        );
        $session['sticky_notes'] = array_slice( $notes, -12 );
        $session['updated_at']   = time();
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'   => 'sticky_note',
                'text'   => $text,
                'source' => sanitize_key( (string) $source ),
                'ts'     => time(),
            )
        );

        return true;
    }

    public function reset_session( $session_id, $preserve_notes = true ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return false;
        }

        $current = $this->get_session( $session_id );
        $this->maybe_flush_session_memory( $session_id, $current, 'reset' );
        $reset   = array(
            'updated_at'       => time(),
            'mode'             => isset( $current['mode'] ) ? (string) $current['mode'] : 'main',
            'turn_count'       => 0,
            'compaction_count' => 0,
            'session_label'    => isset( $current['session_label'] ) ? (string) $current['session_label'] : '',
            'session_scope'    => isset( $current['session_scope'] ) ? (string) $current['session_scope'] : '',
            'subagent_depth'   => max( 0, intval( isset( $current['subagent_depth'] ) ? $current['subagent_depth'] : 0 ) ),
            'allow_writes'     => ! empty( $current['allow_writes'] ),
            'parent_session_id' => isset( $current['parent_session_id'] ) ? (string) $current['parent_session_id'] : '',
            'parent_turn_id'   => isset( $current['parent_turn_id'] ) ? (string) $current['parent_turn_id'] : '',
            'last_turn_id'     => '',
            'worklog_summary'  => '',
            'session_ram'      => $this->get_default_session_ram(),
            'sticky_notes'     => $preserve_notes && ! empty( $current['sticky_notes'] ) ? array_values( (array) $current['sticky_notes'] ) : array(),
            'turns'            => array(),
            'recent_subagents' => array(),
            'active_tools'     => array(),
            'extension_workflow' => array(),
            'task_attempt_budget' => array(),
        );
        $this->save_session( $session_id, $reset );
        if ( $this->workspace && method_exists( $this->workspace, 'get_session_actor_state_path' ) ) {
            $actor_state_path = (string) $this->workspace->get_session_actor_state_path( $session_id );
            if ( '' !== $actor_state_path && file_exists( $actor_state_path ) ) {
                @unlink( $actor_state_path );
            }
        }
        $transcript_path = $this->get_transcript_path( $session_id );
        if ( '' !== $transcript_path && file_exists( $transcript_path ) ) {
            @unlink( $transcript_path );
        }
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'           => 'session_reset',
                'preserve_notes' => ! empty( $preserve_notes ),
                'ts'             => time(),
            )
        );

        return true;
    }

    public function compact_session( $session_id, $instructions = '' ) {
        $session_id   = trim( (string) $session_id );
        $instructions = trim( (string) $instructions );
        if ( '' === $session_id ) {
            return array(
                'success' => false,
                'error'   => 'Session ID je prazan.',
            );
        }

        $session = $this->get_session( $session_id );
        $turns   = isset( $session['turns'] ) && is_array( $session['turns'] ) ? $session['turns'] : array();
        if ( empty( $turns ) && '' === trim( (string) ( isset( $session['worklog_summary'] ) ? $session['worklog_summary'] : '' ) ) ) {
            return array(
                'success' => true,
                'summary' => '',
                'count'   => 0,
                'note'    => 'empty_session',
            );
        }

        $this->maybe_flush_session_memory( $session_id, $session, 'manual_compaction' );
        $session['worklog_summary'] = $this->build_compacted_summary(
            isset( $session['worklog_summary'] ) ? (string) $session['worklog_summary'] : '',
            $turns,
            $instructions
        );
        $session['turns']            = array_slice( $turns, -2 );
        $session['compaction_count'] = intval( isset( $session['compaction_count'] ) ? $session['compaction_count'] : 0 ) + 1;
        $session['updated_at']       = time();
        $this->save_session( $session_id, $session );
        $this->append_transcript_entry(
            $session_id,
            array(
                'type'             => 'session_compaction',
                'instructions'     => $instructions,
                'compaction_count' => intval( $session['compaction_count'] ),
                'summary'          => (string) $session['worklog_summary'],
                'ts'               => time(),
            )
        );

        return array(
            'success'          => true,
            'summary'          => (string) $session['worklog_summary'],
            'compaction_count' => intval( $session['compaction_count'] ),
            'kept_turns'       => count( $session['turns'] ),
        );
    }

    public function get_session_transcript_excerpt( $session_id, $limit = 20 ) {
        $records = array();
        if ( $this->workspace && method_exists( $this->workspace, 'read_session_transcript' ) ) {
            $records = $this->workspace->read_session_transcript( $session_id, $limit );
        }
        if ( empty( $records ) ) {
            return array();
        }

        $lines = array();
        foreach ( $records as $record ) {
            if ( ! is_array( $record ) ) {
                continue;
            }
            if ( $this->is_internal_history_record( $record ) ) {
                continue;
            }

            $type = isset( $record['type'] ) ? (string) $record['type'] : 'event';
            if ( 'turn_start' === $type ) {
                $lines[] = 'USER: ' . $this->truncate_text( isset( $record['prompt'] ) ? (string) $record['prompt'] : '', 220 );
            } elseif ( 'turn_end' === $type ) {
                $label = $this->is_automated_delivery_record( $record ) ? 'AUTOMATED DELIVERY (not a chat reply)' : 'ASSISTANT';
                $lines[] = $label . ': ' . $this->truncate_text( isset( $record['reply'] ) ? (string) $record['reply'] : '', 220 );
            } elseif ( 'tool' === $type ) {
                $lines[] = 'TOOL ' . ( isset( $record['tool'] ) ? (string) $record['tool'] : 'unknown' ) . ': ' . $this->truncate_text( $this->encode_compact_json( isset( $record['output'] ) ? $record['output'] : array() ), 200 );
            } elseif ( 'sticky_note' === $type ) {
                $lines[] = 'NOTE: ' . $this->truncate_text( isset( $record['text'] ) ? (string) $record['text'] : '', 220 );
            } elseif ( 'subagent_result' === $type ) {
                $sub     = isset( $record['subagent_record'] ) && is_array( $record['subagent_record'] ) ? $record['subagent_record'] : array();
                $lines[] = 'SUBAGENT: ' . $this->truncate_text( isset( $sub['summary'] ) ? (string) $sub['summary'] : '', 220 );
            }
        }

	        return array_values( array_filter( $lines ) );
	    }

	    public function append_ad_hoc_turn_transcript( $session_id, $prompt, $reply, $meta = array(), $status = 'completed' ) {
	        $session_id = trim( (string) $session_id );
	        $prompt     = (string) $prompt;
	        $reply      = (string) $reply;
	        if ( '' === $session_id || ( '' === trim( $prompt ) && '' === trim( $reply ) ) ) {
	            return;
	        }

	        $turn_id = $this->generate_turn_id();
	        $meta    = $this->sanitize_meta( $meta );
	        $ts      = time();

	        if ( '' !== trim( $prompt ) ) {
	            $this->append_transcript_entry(
	                $session_id,
	                array(
	                    'type'    => 'turn_start',
	                    'turn_id' => $turn_id,
	                    'mode'    => 'adhoc',
	                    'prompt'  => $prompt,
	                    'meta'    => $meta,
	                    'ts'      => $ts,
	                )
	            );
	        }

	        if ( '' !== trim( $reply ) ) {
	            $this->append_transcript_entry(
	                $session_id,
	                array(
	                    'type'    => 'turn_end',
	                    'turn_id' => $turn_id,
	                    'status'  => sanitize_key( (string) $status ),
	                    'reply'   => $reply,
	                    'meta'    => $meta,
	                    'ts'      => time(),
	                )
	            );
	        }
	    }

	    /* ================================================================
	     * Private helpers
     * ============================================================= */

    private function compact_session_if_needed( $session_id, $session ) {
        $turns = isset( $session['turns'] ) && is_array( $session['turns'] ) ? $session['turns'] : array();
        if ( count( $turns ) <= self::MAX_RECENT_TURNS ) {
            return $session;
        }

        $keep  = array_slice( $turns, -self::MAX_RECENT_TURNS );
        $older = array_slice( $turns, 0, -self::MAX_RECENT_TURNS );
        if ( empty( $older ) ) {
            $session['turns'] = $keep;
            return $session;
        }

        $session['worklog_summary'] = $this->build_compacted_summary(
            isset( $session['worklog_summary'] ) ? (string) $session['worklog_summary'] : '',
            $older
        );
        $session['turns']            = $keep;
        $session['compaction_count'] = intval( isset( $session['compaction_count'] ) ? $session['compaction_count'] : 0 ) + 1;
        $this->maybe_flush_session_memory( $session_id, $session, 'auto_compaction' );

        $this->append_transcript_entry(
            $session_id,
            array(
                'type'             => 'auto_compaction',
                'compaction_count' => intval( $session['compaction_count'] ),
                'summary'          => (string) $session['worklog_summary'],
                'ts'               => time(),
            )
        );

        return $session;
    }

    private function build_compacted_summary( $existing_summary, $older_turns, $instructions = '' ) {
        $older_text = $this->format_turns_for_compaction( $older_turns );
        if ( '' === $older_text ) {
            return $existing_summary;
        }

        if ( ! $this->auth_manager || ! $this->openai_client ) {
            return $this->truncate_text( trim( $existing_summary . "\n" . $older_text . "\n" . $instructions ), 2400 );
        }

        $auth_mode = $this->auth_manager->get_mode();
        $creds     = $this->auth_manager->get_credentials();
        $model     = $this->model_router && method_exists( $this->model_router, 'route_for_founder_agent' )
            ? $this->model_router->route_for_founder_agent()
            : ( $this->model_router ? $this->model_router->route_for_agent() : 'gpt-5.4-mini' );
        $prompt    = "Existing worklog summary:\n" . ( '' !== trim( (string) $existing_summary ) ? trim( (string) $existing_summary ) : 'None yet.' ) . "\n\nOlder completed turns:\n" . $older_text;
        if ( '' !== $instructions ) {
            $prompt .= "\n\nExtra compaction instructions:\n" . $instructions;
        }

        $result = $this->openai_client->generate(
            $model,
            $prompt,
            implode(
                "\n",
                array(
                    'Summarize prior internal founder-agent turns into a compact worklog.',
                    'Keep only durable facts: completed site changes, important failed attempts, proven file locations, plugin facts, database findings, open technical threads, and stable founder preferences.',
                    'Never dump raw large tool payloads or repetitive chatter.',
                    'Write a short internal summary. Max 260 words.',
                )
            ),
            $auth_mode,
            $creds,
            25
        );

        if ( empty( $result['success'] ) ) {
            return $this->truncate_text( trim( $existing_summary . "\n" . $older_text . "\n" . $instructions ), 2400 );
        }

        return $this->truncate_text( trim( (string) $result['answer'] ), 2400 );
    }

    private function format_turns_for_compaction( $turns ) {
        $lines = array();
        foreach ( (array) $turns as $turn ) {
            if ( ! is_array( $turn ) ) {
                continue;
            }
            if ( $this->is_internal_history_record( $turn ) ) {
                continue;
            }

            $prompt = isset( $turn['prompt'] ) ? trim( (string) $turn['prompt'] ) : '';
            $reply  = isset( $turn['reply'] ) ? trim( (string) $turn['reply'] ) : '';
            $status = isset( $turn['status'] ) ? (string) $turn['status'] : 'completed';
            $tools  = $this->extract_turn_tool_names( $turn );
            if ( '' === $prompt && '' === $reply ) {
                continue;
            }

            $line = strtoupper( isset( $turn['mode'] ) ? (string) $turn['mode'] : 'main' ) . ' ' . $status . ': ' . $prompt;
            if ( ! empty( $tools ) ) {
                $line .= ' | tools=' . implode( ', ', $tools );
            }
            if ( '' !== $reply ) {
                $line .= ' | reply=' . $reply;
            }
            $lines[] = $this->truncate_text( $line, 420 );
        }

        return implode( "\n", array_slice( $lines, -10 ) );
    }

    private function build_recent_turn_log( $session ) {
        $turns = isset( $session['turns'] ) && is_array( $session['turns'] ) ? $session['turns'] : array();
        if ( empty( $turns ) ) {
            return '';
        }

        $lines = array();
        for ( $index = count( $turns ) - 1; $index >= 0 && count( $lines ) < self::MAX_RECENT_TURN_LOG_LINES; $index-- ) {
            $turn = $turns[ $index ];
            if ( ! is_array( $turn ) ) {
                continue;
            }
            if ( $this->is_internal_history_record( $turn ) ) {
                continue;
            }

            $prompt = isset( $turn['prompt'] ) ? trim( (string) $turn['prompt'] ) : '';
            if ( '' === $prompt ) {
                continue;
            }

            $status = isset( $turn['status'] ) ? (string) $turn['status'] : 'completed';
            $reply  = isset( $turn['reply'] ) ? trim( (string) $turn['reply'] ) : '';
            $tools  = $this->extract_turn_tool_names( $turn );
            $line   = strtoupper( isset( $turn['mode'] ) ? (string) $turn['mode'] : 'main' ) . ' ' . $status . ': ' . $prompt;

            if ( ! empty( $tools ) ) {
                $line .= ' | tools=' . implode( ', ', $tools );
            }
            if ( '' !== $reply ) {
                $line .= ' | outcome=' . $reply;
            }

            array_unshift( $lines, $this->truncate_text( $line, 320 ) );
        }

        return implode( "\n", $lines );
    }

    private function build_recent_subagent_notes( $session ) {
        $items = isset( $session['recent_subagents'] ) && is_array( $session['recent_subagents'] ) ? $session['recent_subagents'] : array();
        if ( empty( $items ) ) {
            return '';
        }

        $lines = array();
        foreach ( array_slice( $items, -4 ) as $item ) {
            if ( ! is_array( $item ) ) {
                continue;
            }

            $label   = isset( $item['label'] ) ? trim( (string) $item['label'] ) : 'subagent';
            $task    = isset( $item['task'] ) ? trim( (string) $item['task'] ) : '';
            $summary = isset( $item['summary'] ) ? trim( (string) $item['summary'] ) : '';
            $status  = isset( $item['status'] ) ? (string) $item['status'] : 'completed';
            $line    = $label . ' ' . $status;
            if ( '' !== $task ) {
                $line .= ': ' . $task;
            }
            if ( '' !== $summary ) {
                $line .= ' | ' . $summary;
            }
            $lines[] = $this->truncate_text( $line, 320 );
        }

        return implode( "\n", $lines );
    }

    private function build_recent_product_context_text( $session ) {
        $items = isset( $session['product_context_records'] ) && is_array( $session['product_context_records'] ) ? $session['product_context_records'] : array();
        if ( empty( $items ) ) {
            return '';
        }

        $lines = array();
        foreach ( array_slice( $items, -4 ) as $item ) {
            if ( ! is_array( $item ) ) {
                continue;
            }

            $tool       = isset( $item['tool'] ) ? trim( (string) $item['tool'] ) : 'product_context';
            $label      = isset( $item['label'] ) ? trim( (string) $item['label'] ) : '';
            $query      = isset( $item['query'] ) ? trim( (string) $item['query'] ) : '';
            $product_id = isset( $item['product_id'] ) ? intval( $item['product_id'] ) : 0;
            $line       = strtoupper( $tool );

            if ( '' !== $label ) {
                $line .= ': ' . $label;
            } elseif ( '' !== $query ) {
                $line .= ': ' . $query;
            }

            if ( $product_id > 0 ) {
                $line .= ' | product_id=' . $product_id;
            }

            $variation_ids = isset( $item['variation_ids'] ) && is_array( $item['variation_ids'] ) ? array_values( array_filter( array_map( 'intval', $item['variation_ids'] ) ) ) : array();
            if ( ! empty( $variation_ids ) ) {
                $line .= ' | variation_ids=' . implode( ',', array_slice( $variation_ids, 0, 8 ) );
            }

            $candidate_labels = array();
            foreach ( array_slice( isset( $item['candidates'] ) && is_array( $item['candidates'] ) ? $item['candidates'] : array(), 0, 3 ) as $candidate ) {
                if ( ! is_array( $candidate ) ) {
                    continue;
                }

                $candidate_id    = isset( $candidate['post_id'] ) ? intval( $candidate['post_id'] ) : 0;
                $candidate_title = isset( $candidate['post_title'] ) ? trim( (string) $candidate['post_title'] ) : '';
                if ( $candidate_id <= 0 && '' === $candidate_title ) {
                    continue;
                }

                $candidate_labels[] = ( $candidate_id > 0 ? '#' . $candidate_id . ' ' : '' ) . $candidate_title;
            }

            if ( ! empty( $candidate_labels ) ) {
                $line .= ' | candidates=' . implode( ' ; ', $candidate_labels );
            }

            $lines[] = $this->truncate_text( $line, 340 );
        }

        return implode( "\n", $lines );
    }

    private function build_sticky_notes_text( $session ) {
        $notes = isset( $session['sticky_notes'] ) && is_array( $session['sticky_notes'] ) ? $session['sticky_notes'] : array();
        if ( empty( $notes ) ) {
            return '';
        }

        $lines = array();
        foreach ( array_slice( $notes, -6 ) as $note ) {
            if ( ! is_array( $note ) ) {
                continue;
            }

            $text = isset( $note['text'] ) ? trim( (string) $note['text'] ) : '';
            if ( '' === $text ) {
                continue;
            }
            $lines[] = $this->truncate_text( $text, 240 );
        }

        return implode( "\n", $lines );
    }

    private function build_active_tools_text( $session ) {
        $items = isset( $session['active_tools'] ) && is_array( $session['active_tools'] ) ? $session['active_tools'] : array();
        if ( empty( $items ) ) {
            return '';
        }

        $lines = array();
        foreach ( array_slice( array_keys( $items ), -12 ) as $name ) {
            $lines[] = '- ' . $name;
        }

        return "Active tools in this session:\n" . implode( "\n", $lines );
    }

    private function build_extension_workflow_context_text( $session ) {
        $workflow = isset( $session['extension_workflow'] ) && is_array( $session['extension_workflow'] )
            ? $this->normalize_extension_workflow_state( $session['extension_workflow'] )
            : array();
        if ( empty( $workflow ) || empty( $workflow['status'] ) ) {
            return '';
        }
        $lines   = array();
        $status  = isset( $workflow['status'] ) ? (string) $workflow['status'] : '';
        if ( in_array( $status, array( 'completed', 'failed', 'denied' ), true ) ) {
            return '';
        }
        if ( in_array( $status, array( 'awaiting_approval', 'approved' ), true ) && $this->extension_workflow_has_later_turn( $session, $workflow ) ) {
            return '';
        }
        $kind = 'self_extension';
        $lines[] = 'Extension workflow status: ' . $status;
        $lines[] = 'Workflow kind: ' . $kind;
        if ( ! empty( $workflow['missing_capability'] ) ) {
            $lines[] = 'Missing capability: ' . (string) $workflow['missing_capability'];
        }
        if ( ! empty( $workflow['blocked_tool'] ) ) {
            $lines[] = 'Blocked tool: ' . (string) $workflow['blocked_tool'];
        }
        if ( ! empty( $workflow['failed_tool'] ) ) {
            $lines[] = 'Failed tool: ' . (string) $workflow['failed_tool'];
        }
        if ( ! empty( $workflow['original_prompt'] ) ) {
            $lines[] = 'Original task: ' . $this->truncate_text( (string) $workflow['original_prompt'], 360 );
        }
        if ( ! empty( $workflow['implementation_brief'] ) ) {
            $lines[] = 'Implementation brief: ' . $this->truncate_text( (string) $workflow['implementation_brief'], 360 );
        }
        if ( ! empty( $workflow['adequacy_evidence'] ) ) {
            $lines[] = 'Adequacy evidence: ' . $this->truncate_text( (string) $workflow['adequacy_evidence'], 360 );
        }
        if ( ! empty( $workflow['selected_external_skill'] ) ) {
            $lines[] = 'Selected external skill: ' . (string) $workflow['selected_external_skill'];
        }
        if ( ! empty( $workflow['retry_original_task'] ) ) {
            $lines[] = 'Retry original task after capability is ready: yes';
        }

        return implode( "\n", $lines );
    }

    private function build_task_attempt_budget_context_text( $session ) {
        $state = isset( $session['task_attempt_budget'] ) && is_array( $session['task_attempt_budget'] )
            ? $this->normalize_task_attempt_budget_state( $session['task_attempt_budget'] )
            : array();
        if ( empty( $state ) || empty( $state['status'] ) ) {
            return '';
        }

        $lines   = array();
        $status  = isset( $state['status'] ) ? (string) $state['status'] : '';
        $attempt = intval( isset( $state['attempt_count'] ) ? $state['attempt_count'] : 0 );
        $max     = intval( isset( $state['max_attempts'] ) ? $state['max_attempts'] : 2 );

        $lines[] = 'Task attempt budget status: ' . $status;
        $lines[] = 'Automatic recovery attempts used: ' . $attempt . '/' . $max;
        if ( ! empty( $state['original_task'] ) ) {
            $lines[] = 'Original task: ' . $this->truncate_text( (string) $state['original_task'], 300 );
        }
        if ( ! empty( $state['last_blocker'] ) ) {
            $lines[] = 'Last blocker: ' . $this->truncate_text( (string) $state['last_blocker'], 240 );
        }
        if ( ! empty( $state['attempted_paths'] ) && is_array( $state['attempted_paths'] ) ) {
            $lines[] = 'Attempted paths: ' . implode( ', ', array_slice( array_map( 'strval', $state['attempted_paths'] ), 0, 6 ) );
        }
        if ( 'blocked_after_two_attempts' === $status ) {
            $lines[] = 'Do not continue this same task from heartbeat or continuation without a new founder message or explicit approval.';
        }

        return implode( "\n", $lines );
    }

    private function extension_workflow_has_later_turn( $session, $workflow ) {
        $session  = is_array( $session ) ? $session : array();
        $workflow = is_array( $workflow ) ? $workflow : array();
        $updated  = intval( isset( $workflow['updated_at'] ) ? $workflow['updated_at'] : 0 );
        $request_turn_id = isset( $workflow['request_turn_id'] ) ? trim( (string) $workflow['request_turn_id'] ) : '';
        if ( $updated <= 0 ) {
            return false;
        }

        $turns = isset( $session['turns'] ) && is_array( $session['turns'] ) ? $session['turns'] : array();
        foreach ( $turns as $turn ) {
            if ( ! is_array( $turn ) ) {
                continue;
            }

            $turn_id = isset( $turn['id'] ) ? trim( (string) $turn['id'] ) : '';
            if ( '' !== $request_turn_id && $request_turn_id === $turn_id ) {
                continue;
            }

            $ended = intval( isset( $turn['ended_at'] ) ? $turn['ended_at'] : 0 );
            if ( $ended > $updated ) {
                return true;
            }
        }

        return false;
    }

    private function extract_turn_tool_names( $turn ) {
        $names = array();
        foreach ( isset( $turn['tool_events'] ) && is_array( $turn['tool_events'] ) ? $turn['tool_events'] : array() as $event ) {
            if ( empty( $event['name'] ) ) {
                continue;
            }
            $names[] = (string) $event['name'];
        }

        return array_values( array_unique( array_filter( $names ) ) );
    }

    private function summarize_tool_output( $output ) {
        if ( is_array( $output ) ) {
            foreach ( array( 'summary', 'error', 'detail', 'reply', 'message' ) as $key ) {
                if ( ! empty( $output[ $key ] ) ) {
                    return $this->truncate_text( (string) $output[ $key ], 320 );
                }
            }

            if ( isset( $output['results'] ) && is_array( $output['results'] ) ) {
                $count = count( $output['results'] );
                return $count > 0 ? 'results=' . $count : 'results=0';
            }

            if ( isset( $output['success'] ) ) {
                return ! empty( $output['success'] ) ? 'success' : 'failed';
            }
        }

        return $this->truncate_text( $this->encode_compact_json( $output ), 320 );
    }

    private function sanitize_meta( $meta ) {
        $meta = is_array( $meta ) ? $meta : array();

        foreach ( $meta as $key => $value ) {
            if ( is_array( $value ) ) {
                $meta[ $key ] = $this->truncate_text( $this->encode_compact_json( $value ), 320 );
            } elseif ( is_bool( $value ) ) {
                $meta[ $key ] = $value ? 'true' : 'false';
            } else {
                $meta[ $key ] = $this->truncate_text( (string) $value, 320 );
            }
        }

        return $meta;
    }

    private function sanitize_tool_name_list( $tool_names ) {
        $names = is_array( $tool_names ) ? $tool_names : array( $tool_names );
        $clean = array();
        foreach ( $names as $name ) {
            $name = sanitize_key( (string) $name );
            if ( '' === $name ) {
                continue;
            }
            $clean[] = $name;
        }

        return array_values( array_unique( $clean ) );
    }

    private function maybe_capture_turn_learning( $session_id, $session, $turn ) {
        if ( ! $this->self_improvement_manager || ! method_exists( $this->self_improvement_manager, 'capture_turn_learning' ) ) {
            return;
        }

        $this->self_improvement_manager->capture_turn_learning( $session_id, $session, $turn );
    }

    private function maybe_flush_session_memory( $session_id, $session, $reason ) {
        if ( ! $this->self_improvement_manager || ! method_exists( $this->self_improvement_manager, 'flush_session_memory' ) ) {
            return;
        }

        $this->self_improvement_manager->flush_session_memory( $session_id, $session, $reason );
    }

    private function get_default_session_ram() {
        return array(
            'current_topic'    => '',
            'immediate_goals'  => array(),
            'recent_progress'  => array(),
            'next_steps'       => array(),
            'user_state'       => '',
            'restart_recap'    => '',
        );
    }

    private function normalize_session_ram( $session_ram ) {
        $session_ram = wp_parse_args( is_array( $session_ram ) ? $session_ram : array(), $this->get_default_session_ram() );

        return array(
            'current_topic'   => $this->truncate_text( isset( $session_ram['current_topic'] ) ? (string) $session_ram['current_topic'] : '', 220 ),
            'immediate_goals' => $this->normalize_session_ram_list( isset( $session_ram['immediate_goals'] ) ? $session_ram['immediate_goals'] : array(), 5, 180 ),
            'recent_progress' => $this->normalize_session_ram_list( isset( $session_ram['recent_progress'] ) ? $session_ram['recent_progress'] : array(), 6, 180 ),
            'next_steps'      => $this->normalize_session_ram_list( isset( $session_ram['next_steps'] ) ? $session_ram['next_steps'] : array(), 5, 180 ),
            'user_state'      => $this->truncate_text( isset( $session_ram['user_state'] ) ? (string) $session_ram['user_state'] : '', 220 ),
            'restart_recap'   => $this->truncate_text( isset( $session_ram['restart_recap'] ) ? (string) $session_ram['restart_recap'] : '', 420 ),
        );
    }

    private function normalize_session_ram_list( $items, $max_items = 5, $limit = 180 ) {
        $items = is_array( $items ) ? $items : array();
        $out   = array();

        foreach ( $items as $item ) {
            $item = $this->truncate_text( trim( preg_replace( '/\s+/u', ' ', (string) $item ) ), $limit );
            if ( '' === $item ) {
                continue;
            }
            $out[] = $item;
            if ( count( $out ) >= max( 1, intval( $max_items ) ) ) {
                break;
            }
        }

        return $out;
    }

    private function build_session_ram_list_text( $items ) {
        $items = $this->normalize_session_ram_list( $items, 6, 180 );
        if ( empty( $items ) ) {
            return '';
        }

        return implode( "\n", array_map( function ( $item ) {
            return '- ' . $item;
        }, $items ) );
    }

    private function refresh_structured_session_ram( $session_id, $session, $turn = array() ) {
        $session_id = trim( (string) $session_id );
        $session    = is_array( $session ) ? $session : array();
        $turn       = is_array( $turn ) ? $turn : array();
        $existing   = isset( $session['session_ram'] ) && is_array( $session['session_ram'] ) ? $session['session_ram'] : array();
        $fallback   = $this->build_session_ram_fallback( $session, $turn );

        if ( ! $this->auth_manager || ! $this->openai_client ) {
            $session['session_ram'] = $this->normalize_session_ram( array_merge( $existing, $fallback ) );
            return $session;
        }

        $recent_turn_log = $this->build_recent_turn_log( $session );
        $prompt          = wp_json_encode(
            array(
                'session_id'       => $session_id,
                'existing_summary' => isset( $session['worklog_summary'] ) ? (string) $session['worklog_summary'] : '',
                'sticky_notes'     => isset( $session['sticky_notes'] ) ? array_values( (array) $session['sticky_notes'] ) : array(),
                'recent_turns'     => array_slice( isset( $session['turns'] ) ? array_values( (array) $session['turns'] ) : array(), -4 ),
                'current_turn'     => $turn,
                'existing_ram'     => $existing,
                'fallback_ram'     => $fallback,
                'recent_turn_log'  => $recent_turn_log,
            )
        );
        $model = $this->model_router && method_exists( $this->model_router, 'route_for_founder_agent' )
            ? $this->model_router->route_for_founder_agent()
            : ( $this->model_router ? $this->model_router->route_for_agent() : 'gpt-5.4-mini' );
        $result = $this->openai_client->generate(
            $model,
            $prompt,
            implode(
                "\n",
                array(
                    'Extract structured current-session RAM for Niwa.',
                    'Return minified JSON only with keys: current_topic, immediate_goals, recent_progress, next_steps, user_state, restart_recap.',
                    'current_topic: one short sentence about the active thread.',
                    'immediate_goals: array of current near-term goals.',
                    'recent_progress: array of things already moved forward.',
                    'next_steps: array of likely next actions.',
                    'user_state: one short sentence about founder mood/energy/stance only if reasonably supported, otherwise empty.',
                    'restart_recap: compact restart-safe recap for continuing later.',
                    'Keep it factual, concise, and grounded in the provided turns only.',
                    'Do not invent emotions or plans.',
                )
            ),
            $this->auth_manager->get_mode(),
            $this->auth_manager->get_credentials(),
            20
        );

        if ( empty( $result['success'] ) || empty( $result['answer'] ) ) {
            $session['session_ram'] = $this->normalize_session_ram( array_merge( $existing, $fallback ) );
            return $session;
        }

        $answer  = trim( (string) $result['answer'] );
        $decoded = json_decode( $answer, true );
        if ( ! is_array( $decoded ) && preg_match( '/\{.*\}/s', $answer, $matches ) ) {
            $decoded = json_decode( (string) $matches[0], true );
        }

        if ( ! is_array( $decoded ) ) {
            $session['session_ram'] = $this->normalize_session_ram( array_merge( $existing, $fallback ) );
            return $session;
        }

        $session['session_ram'] = $this->normalize_session_ram( array_merge( $existing, $fallback, $decoded ) );
        return $session;
    }

    private function build_session_ram_fallback( $session, $turn = array() ) {
        $session = is_array( $session ) ? $session : array();
        $turn    = is_array( $turn ) ? $turn : array();
        $turns   = isset( $session['turns'] ) && is_array( $session['turns'] ) ? array_values( $session['turns'] ) : array();
        $recent  = array_slice( $turns, -3 );
        $last    = ! empty( $recent ) ? end( $recent ) : $turn;
        $prompt  = isset( $last['prompt'] ) ? trim( (string) $last['prompt'] ) : '';
        $reply   = isset( $last['reply'] ) ? trim( (string) $last['reply'] ) : '';
        $worklog = isset( $session['worklog_summary'] ) ? trim( (string) $session['worklog_summary'] ) : '';
        $notes   = isset( $session['sticky_notes'] ) && is_array( $session['sticky_notes'] ) ? array_slice( $session['sticky_notes'], -3 ) : array();
        $note_texts = array();
        foreach ( $notes as $note ) {
            if ( is_array( $note ) && ! empty( $note['text'] ) ) {
                $note_texts[] = trim( (string) $note['text'] );
            }
        }

        return array(
            'current_topic'   => '' !== $prompt ? $this->truncate_text( $prompt, 180 ) : $this->truncate_text( $worklog, 180 ),
            'immediate_goals' => array_filter( array(
                '' !== $prompt ? $this->truncate_text( $prompt, 160 ) : '',
                ! empty( $note_texts[0] ) ? $this->truncate_text( $note_texts[0], 160 ) : '',
            ) ),
            'recent_progress' => array_filter( array(
                '' !== $reply ? $this->truncate_text( $reply, 160 ) : '',
                '' !== $worklog ? $this->truncate_text( $worklog, 160 ) : '',
            ) ),
            'next_steps'      => array_filter( array(
                ! empty( $note_texts[1] ) ? $this->truncate_text( $note_texts[1], 160 ) : '',
            ) ),
            'user_state'      => '',
            'restart_recap'   => $this->truncate_text( trim( implode( ' ', array_filter( array( $worklog, $prompt, $reply ) ) ) ), 360 ),
        );
    }

    private function normalize_active_tools_map( $active_tools ) {
        $active_tools = is_array( $active_tools ) ? $active_tools : array();
        $normalized   = array();
        $now          = time();

        foreach ( $active_tools as $name => $state ) {
            $name = sanitize_key( (string) $name );
            if ( '' === $name ) {
                continue;
            }

            $state     = is_array( $state ) ? $state : array();
            $loaded_at = intval( isset( $state['loaded_at'] ) ? $state['loaded_at'] : 0 );
            $last_used = intval( isset( $state['last_used_at'] ) ? $state['last_used_at'] : 0 );
            $expires   = intval( isset( $state['expires_at'] ) ? $state['expires_at'] : 0 );

            if ( $loaded_at <= 0 ) {
                $loaded_at = $last_used > 0 ? $last_used : $now;
            }
            if ( $last_used <= 0 ) {
                $last_used = $loaded_at;
            }
            if ( $expires <= 0 ) {
                $expires = $last_used + self::DEFAULT_ACTIVE_TOOL_TTL;
            }
            if ( $expires < $now ) {
                continue;
            }

            $normalized[ $name ] = array(
                'loaded_at'    => $loaded_at,
                'last_used_at' => $last_used,
                'expires_at'   => $expires,
                'turn_id'      => isset( $state['turn_id'] ) ? sanitize_text_field( trim( (string) $state['turn_id'] ) ) : '',
            );
        }

        uasort(
            $normalized,
            function ( $a, $b ) {
                return intval( isset( $b['last_used_at'] ) ? $b['last_used_at'] : 0 ) <=> intval( isset( $a['last_used_at'] ) ? $a['last_used_at'] : 0 );
            }
        );

        if ( count( $normalized ) > self::MAX_ACTIVE_TOOLS ) {
            $normalized = array_slice( $normalized, 0, self::MAX_ACTIVE_TOOLS, true );
        }

        return $normalized;
    }

    private function normalize_extension_workflow_state( $workflow ) {
        if ( ! is_array( $workflow ) || empty( $workflow ) ) {
            return array();
        }

        $allowed_statuses = array(
            'awaiting_approval',
            'approved',
            'denied',
            'completed',
            'failed',
        );
        $status = isset( $workflow['status'] ) ? sanitize_key( (string) $workflow['status'] ) : '';
        if ( '' === $status ) {
            $status = 'awaiting_approval';
        }
        if ( ! in_array( $status, $allowed_statuses, true ) ) {
            $status = 'awaiting_approval';
        }

        $original_prompt = $this->truncate_text( isset( $workflow['original_prompt'] ) ? (string) $workflow['original_prompt'] : '', 1200 );
        $original_hash   = isset( $workflow['original_task_hash'] ) ? preg_replace( '/[^a-f0-9]/', '', strtolower( (string) $workflow['original_task_hash'] ) ) : '';
        if ( '' === $original_hash && '' !== $original_prompt ) {
            $original_hash = $this->hash_task_prompt( $original_prompt );
        }

        return array(
            'status'             => $status,
            'workflow_kind'      => 'self_extension',
            'missing_capability' => $this->truncate_text( isset( $workflow['missing_capability'] ) ? sanitize_key( (string) $workflow['missing_capability'] ) : '', 80 ),
            'blocked_tool'       => $this->truncate_text( isset( $workflow['blocked_tool'] ) ? sanitize_key( (string) $workflow['blocked_tool'] ) : '', 80 ),
            'failed_tool'        => $this->truncate_text( isset( $workflow['failed_tool'] ) ? sanitize_key( (string) $workflow['failed_tool'] ) : '', 80 ),
            'error_code'         => $this->truncate_text( isset( $workflow['error_code'] ) ? sanitize_key( (string) $workflow['error_code'] ) : '', 80 ),
            'safe_user_message'  => $this->truncate_text( isset( $workflow['safe_user_message'] ) ? (string) $workflow['safe_user_message'] : '', 500 ),
            'original_prompt'    => $original_prompt,
            'original_task_hash' => $original_hash,
            'implementation_brief' => $this->truncate_text( isset( $workflow['implementation_brief'] ) ? (string) $workflow['implementation_brief'] : '', 700 ),
            'failed_args'        => isset( $workflow['failed_args'] ) && is_array( $workflow['failed_args'] ) ? $this->compact_extension_workflow_array( $workflow['failed_args'], 3000 ) : array(),
            'target_paths'       => isset( $workflow['target_paths'] ) && is_array( $workflow['target_paths'] ) ? array_slice( array_values( array_map( 'strval', $workflow['target_paths'] ) ), 0, 20 ) : array(),
            'attempted_tools'    => $this->normalize_short_text_list( isset( $workflow['attempted_tools'] ) ? $workflow['attempted_tools'] : array(), 12, 120 ),
            'search_query'       => $this->truncate_text( isset( $workflow['search_query'] ) ? (string) $workflow['search_query'] : '', 240 ),
            'candidate_tools'    => $this->normalize_short_text_list( isset( $workflow['candidate_tools'] ) ? $workflow['candidate_tools'] : array(), 12, 120 ),
            'risk_level'         => in_array( isset( $workflow['risk_level'] ) ? sanitize_key( (string) $workflow['risk_level'] ) : '', array( 'low', 'medium', 'high' ), true ) ? sanitize_key( (string) $workflow['risk_level'] ) : '',
            'retry_same_session' => array_key_exists( 'retry_same_session', $workflow ) ? ! empty( $workflow['retry_same_session'] ) : null,
            'learning_plan'      => $this->truncate_text( isset( $workflow['learning_plan'] ) ? (string) $workflow['learning_plan'] : '', 900 ),
            'adequacy_evidence'  => $this->truncate_text( isset( $workflow['adequacy_evidence'] ) ? (string) $workflow['adequacy_evidence'] : '', 900 ),
            'external_skill_candidates' => $this->normalize_short_text_list( isset( $workflow['external_skill_candidates'] ) ? $workflow['external_skill_candidates'] : array(), 12, 160 ),
            'selected_external_skill' => $this->truncate_text( isset( $workflow['selected_external_skill'] ) ? (string) $workflow['selected_external_skill'] : '', 220 ),
            'reason_skill_not_enough' => $this->truncate_text( isset( $workflow['reason_skill_not_enough'] ) ? (string) $workflow['reason_skill_not_enough'] : '', 700 ),
            'reason_tool_needed' => $this->truncate_text( isset( $workflow['reason_tool_needed'] ) ? (string) $workflow['reason_tool_needed'] : '', 700 ),
            'retry_original_task' => array_key_exists( 'retry_original_task', $workflow ) ? ! empty( $workflow['retry_original_task'] ) : null,
            'approval_note'      => $this->truncate_text( isset( $workflow['approval_note'] ) ? (string) $workflow['approval_note'] : '', 500 ),
            'request_turn_id'    => $this->truncate_text( isset( $workflow['request_turn_id'] ) ? sanitize_text_field( (string) $workflow['request_turn_id'] ) : '', 80 ),
            'created_at'         => intval( isset( $workflow['created_at'] ) ? $workflow['created_at'] : 0 ),
            'updated_at'         => intval( isset( $workflow['updated_at'] ) ? $workflow['updated_at'] : 0 ),
        );
    }

    private function normalize_short_text_list( $items, $limit = 12, $item_limit = 120 ) {
        $items = is_array( $items ) ? $items : array();
        $out   = array();
        foreach ( $items as $item ) {
            $item = $this->truncate_text( sanitize_text_field( (string) $item ), $item_limit );
            if ( '' !== $item ) {
                $out[] = $item;
            }
            if ( count( $out ) >= max( 1, intval( $limit ) ) ) {
                break;
            }
        }

        return array_values( array_unique( $out ) );
    }

    private function normalize_task_attempt_budget_state( $state ) {
        if ( ! is_array( $state ) || empty( $state ) ) {
            return array();
        }

        $allowed_statuses = array(
            'idle',
            'recovering',
            'adequate',
            'partial',
            'needs_more_research',
            'skill_gap',
            'tool_gap',
            'verification_unavailable',
            'pending_subagent',
            'unsafe_or_disallowed',
            'not_feasible',
            'blocked_after_two_attempts',
        );
        $status = isset( $state['status'] ) ? sanitize_key( (string) $state['status'] ) : 'idle';
        if ( ! in_array( $status, $allowed_statuses, true ) ) {
            $status = 'idle';
        }

        $attempted_paths = array();
        foreach ( isset( $state['attempted_paths'] ) && is_array( $state['attempted_paths'] ) ? $state['attempted_paths'] : array() as $path ) {
            $path = $this->truncate_text( sanitize_text_field( (string) $path ), 120 );
            if ( '' !== $path ) {
                $attempted_paths[] = $path;
            }
            if ( count( $attempted_paths ) >= 12 ) {
                break;
            }
        }

        return array(
            'original_task_hash' => isset( $state['original_task_hash'] ) ? preg_replace( '/[^a-f0-9]/', '', strtolower( (string) $state['original_task_hash'] ) ) : '',
            'task_identity_hash' => isset( $state['task_identity_hash'] ) ? preg_replace( '/[^a-f0-9]/', '', strtolower( (string) $state['task_identity_hash'] ) ) : '',
            'original_task'      => $this->truncate_text( isset( $state['original_task'] ) ? (string) $state['original_task'] : '', 1200 ),
            'attempt_count'      => max( 0, intval( isset( $state['attempt_count'] ) ? $state['attempt_count'] : 0 ) ),
            'max_attempts'       => max( 1, min( 4, intval( isset( $state['max_attempts'] ) ? $state['max_attempts'] : 2 ) ) ),
            'last_blocker'       => $this->truncate_text( isset( $state['last_blocker'] ) ? (string) $state['last_blocker'] : '', 700 ),
            'attempted_paths'    => array_values( array_unique( $attempted_paths ) ),
            'status'             => $status,
            'created_at'         => intval( isset( $state['created_at'] ) ? $state['created_at'] : 0 ),
            'updated_at'         => intval( isset( $state['updated_at'] ) ? $state['updated_at'] : 0 ),
        );
    }

    private function compact_extension_workflow_array( $value, $limit ) {
        $json = wp_json_encode( $value );
        if ( ! is_string( $json ) || strlen( $json ) <= $limit ) {
            return $value;
        }

        return array(
            '_truncated' => true,
            'preview'    => substr( $json, 0, max( 120, intval( $limit ) - 32 ) ),
        );
    }

    private function choose_latest_extension_workflow( $current, $incoming ) {
        $current  = $this->normalize_extension_workflow_state( $current );
        $incoming = $this->normalize_extension_workflow_state( $incoming );
        if ( empty( $current ) ) {
            return $incoming;
        }
        if ( empty( $incoming ) ) {
            return $current;
        }

        return intval( isset( $incoming['updated_at'] ) ? $incoming['updated_at'] : 0 ) >= intval( isset( $current['updated_at'] ) ? $current['updated_at'] : 0 )
            ? $incoming
            : $current;
    }

    private function choose_latest_task_attempt_budget( $current, $incoming ) {
        $current  = $this->normalize_task_attempt_budget_state( $current );
        $incoming = $this->normalize_task_attempt_budget_state( $incoming );
        if ( empty( $current ) ) {
            return $incoming;
        }
        if ( empty( $incoming ) ) {
            return $current;
        }

        return intval( isset( $incoming['updated_at'] ) ? $incoming['updated_at'] : 0 ) >= intval( isset( $current['updated_at'] ) ? $current['updated_at'] : 0 )
            ? $incoming
            : $current;
    }

    private function hash_task_prompt( $prompt ) {
        $prompt = trim( preg_replace( '/\s+/', ' ', (string) $prompt ) );
        if ( '' === $prompt ) {
            return '';
        }

        return hash( 'sha256', $prompt );
    }

    private function get_all_sessions() {
        $sessions = array();

        if ( $this->has_workspace_session_backend() ) {
            $this->maybe_migrate_legacy_option_state_to_workspace();
            foreach ( (array) $this->workspace->list_session_manifests( self::MAX_SESSIONS * 2 ) as $manifest ) {
                if ( ! is_array( $manifest ) || empty( $manifest['session_id'] ) ) {
                    continue;
                }
                $session_id = (string) $manifest['session_id'];
                $sessions[ $session_id ] = $this->normalize_session_record( $session_id, $manifest );
            }
        } else {
            foreach ( $this->load_legacy_option_sessions() as $session_id => $session ) {
                $sessions[ (string) $session_id ] = $this->normalize_session_record( (string) $session_id, $session );
            }
        }

        uasort(
            $sessions,
            function ( $a, $b ) {
                return intval( isset( $b['updated_at'] ) ? $b['updated_at'] : 0 ) <=> intval( isset( $a['updated_at'] ) ? $a['updated_at'] : 0 );
            }
        );

        return array_slice( $sessions, 0, self::MAX_SESSIONS, true );
    }

    private function get_session( $session_id ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array();
        }

        $raw = array();
        if ( $this->has_workspace_session_backend() ) {
            $this->maybe_migrate_legacy_option_state_to_workspace();
            $raw = (array) $this->workspace->read_session_manifest( $session_id );
        }
        if ( empty( $raw ) ) {
            $legacy = $this->load_legacy_option_sessions();
            $raw    = isset( $legacy[ $session_id ] ) && is_array( $legacy[ $session_id ] ) ? $legacy[ $session_id ] : array();
        }

        $raw = $this->normalize_session_record( $session_id, $raw );

        return wp_parse_args(
            $raw,
            array(
                'updated_at'       => 0,
                'mode'             => 'main',
                'turn_count'       => 0,
                'compaction_count' => 0,
                'session_label'    => '',
                'session_scope'    => '',
                'subagent_depth'   => 0,
                'allow_writes'     => false,
                'parent_session_id' => '',
                'parent_turn_id'   => '',
                'last_turn_id'     => '',
                'session_file'     => $this->get_transcript_path( $session_id ),
                'worklog_summary'  => '',
                'session_ram'      => $this->get_default_session_ram(),
                'sticky_notes'             => array(),
                'turns'                    => array(),
                'recent_subagents'         => array(),
                'product_context_records'  => array(),
                'media_context_records'    => array(),
                'active_tools'             => array(),
                'extension_workflow'       => array(),
                'task_attempt_budget'      => array(),
            )
        );
    }

    private function save_session( $session_id, $session ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return;
        }

        $session = $this->normalize_session_record( $session_id, $session );

        if ( ! $this->has_workspace_session_backend() ) {
            $all                = $this->load_legacy_option_sessions();
            $all[ $session_id ] = $session;
            if ( false === get_option( self::OPTION_KEY ) ) {
                add_option( self::OPTION_KEY, $all, '', 'no' );
            } else {
                update_option( self::OPTION_KEY, $all, false );
            }
            return;
        }

        $lock_name = 'manifest:' . $session_id;
        $locked    = false;
        for ( $attempt = 0; $attempt < 12; $attempt++ ) {
            $locked = $this->acquire_session_lane( $lock_name, 20 );
            if ( $locked ) {
                break;
            }
            usleep( 100000 );
        }

        if ( ! $locked ) {
            return;
        }

        try {
            $current = $this->normalize_session_record(
                $session_id,
                (array) $this->workspace->read_session_manifest( $session_id )
            );
            if ( ! empty( $current ) ) {
                $session = $this->merge_session_records( $session_id, $current, $session );
            }

            $this->workspace->write_session_manifest( $session_id, $this->build_session_manifest( $session_id, $session ) );
        } finally {
            $this->release_session_lane( $lock_name );
        }
    }

    private function overwrite_workspace_session( $session_id, $mutator ) {
        $session_id = trim( (string) $session_id );
        if ( '' === $session_id || ! is_callable( $mutator ) || ! $this->has_workspace_session_backend() ) {
            return false;
        }

        $lock_name = 'manifest:' . $session_id;
        $locked    = false;
        for ( $attempt = 0; $attempt < 12; $attempt++ ) {
            $locked = $this->acquire_session_lane( $lock_name, 20 );
            if ( $locked ) {
                break;
            }
            usleep( 100000 );
        }

        if ( ! $locked ) {
            return false;
        }

        try {
            $session = $this->normalize_session_record(
                $session_id,
                (array) $this->workspace->read_session_manifest( $session_id )
            );
            $session = call_user_func( $mutator, $session );
            $session = $this->normalize_session_record( $session_id, is_array( $session ) ? $session : array() );
            $this->workspace->write_session_manifest( $session_id, $this->build_session_manifest( $session_id, $session ) );
            return true;
        } finally {
            $this->release_session_lane( $lock_name );
        }
    }

    private function has_workspace_session_backend() {
        return $this->workspace
            && method_exists( $this->workspace, 'read_session_manifest' )
            && method_exists( $this->workspace, 'write_session_manifest' )
            && method_exists( $this->workspace, 'list_session_manifests' )
            && method_exists( $this->workspace, 'get_session_transcript_path' );
    }

    private function load_legacy_option_sessions() {
        $legacy_state = get_option( self::OPTION_KEY, array() );
        return is_array( $legacy_state ) ? $legacy_state : array();
    }

    private function maybe_migrate_legacy_option_state_to_workspace() {
        if ( $this->legacy_migration_checked ) {
            return;
        }

        $this->legacy_migration_checked = true;
        if ( ! $this->has_workspace_session_backend() ) {
            return;
        }

        $legacy_state = $this->load_legacy_option_sessions();
        if ( empty( $legacy_state ) ) {
            delete_option( self::OPTION_KEY );
            return;
        }

        $migrated = true;
        foreach ( $legacy_state as $session_id => $session ) {
            $session_id = trim( (string) $session_id );
            if ( '' === $session_id || ! is_array( $session ) ) {
                continue;
            }

            $existing = (array) $this->workspace->read_session_manifest( $session_id );
            if ( empty( $existing ) ) {
                $normalized = $this->normalize_session_record( $session_id, $session );
                $migrated   = $this->workspace->write_session_manifest( $session_id, $this->build_session_manifest( $session_id, $normalized ) ) && $migrated;
            }
        }

        if ( $migrated ) {
            delete_option( self::OPTION_KEY );
            wp_cache_delete( self::OPTION_KEY, 'options' );
        }
    }

    private function normalize_session_record( $session_id, $session ) {
        $session_id = trim( (string) $session_id );
        $session    = is_array( $session ) ? $session : array();

        if ( empty( $session['turns'] ) && ! empty( $session['recent_turns'] ) && is_array( $session['recent_turns'] ) ) {
            $session['turns'] = array_values( $session['recent_turns'] );
        }

        if ( empty( $session['session_file'] ) ) {
            $session['session_file'] = $this->get_transcript_path( $session_id );
        }
        $mode = isset( $session['mode'] ) ? sanitize_key( (string) $session['mode'] ) : 'main';
        if ( 'subagent' !== $mode ) {
            $session['allow_writes'] = true;
        }

        $session['active_tools'] = $this->normalize_active_tools_map(
            isset( $session['active_tools'] ) && is_array( $session['active_tools'] ) ? $session['active_tools'] : array()
        );
        $session['extension_workflow'] = $this->normalize_extension_workflow_state(
            isset( $session['extension_workflow'] ) && is_array( $session['extension_workflow'] ) ? $session['extension_workflow'] : array()
        );
        $session['task_attempt_budget'] = $this->normalize_task_attempt_budget_state(
            isset( $session['task_attempt_budget'] ) && is_array( $session['task_attempt_budget'] ) ? $session['task_attempt_budget'] : array()
        );
        $session['session_ram'] = $this->normalize_session_ram(
            isset( $session['session_ram'] ) && is_array( $session['session_ram'] ) ? $session['session_ram'] : array()
        );

        return $session;
    }

    private function merge_session_records( $session_id, $current, $incoming ) {
        $current  = $this->normalize_session_record( $session_id, $current );
        $incoming = $this->normalize_session_record( $session_id, $incoming );

        $session = array_merge( $current, $incoming );

        $session['updated_at']       = max( intval( isset( $current['updated_at'] ) ? $current['updated_at'] : 0 ), intval( isset( $incoming['updated_at'] ) ? $incoming['updated_at'] : 0 ) );
        $session['turn_count']       = max( intval( isset( $current['turn_count'] ) ? $current['turn_count'] : 0 ), intval( isset( $incoming['turn_count'] ) ? $incoming['turn_count'] : 0 ) );
        $session['compaction_count'] = max( intval( isset( $current['compaction_count'] ) ? $current['compaction_count'] : 0 ), intval( isset( $incoming['compaction_count'] ) ? $incoming['compaction_count'] : 0 ) );
        $session['allow_writes']     = ! empty( $current['allow_writes'] ) || ! empty( $incoming['allow_writes'] );
        $session['worklog_summary']  = $this->prefer_longer_text(
            isset( $current['worklog_summary'] ) ? (string) $current['worklog_summary'] : '',
            isset( $incoming['worklog_summary'] ) ? (string) $incoming['worklog_summary'] : ''
        );
        $session['session_ram'] = array_merge(
            isset( $current['session_ram'] ) && is_array( $current['session_ram'] ) ? $current['session_ram'] : array(),
            isset( $incoming['session_ram'] ) && is_array( $incoming['session_ram'] ) ? $incoming['session_ram'] : array()
        );
        $session['sticky_notes'] = $this->merge_session_list(
            isset( $current['sticky_notes'] ) ? (array) $current['sticky_notes'] : array(),
            isset( $incoming['sticky_notes'] ) ? (array) $incoming['sticky_notes'] : array(),
            12,
            array( 'id', 'recorded_at', 'text' )
        );
        $session['recent_subagents'] = $this->merge_session_list(
            isset( $current['recent_subagents'] ) ? (array) $current['recent_subagents'] : array(),
            isset( $incoming['recent_subagents'] ) ? (array) $incoming['recent_subagents'] : array(),
            8,
            array( 'id', 'session_id', 'ts', 'label' )
        );
        $session['product_context_records'] = $this->merge_session_list(
            isset( $current['product_context_records'] ) ? (array) $current['product_context_records'] : array(),
            isset( $incoming['product_context_records'] ) ? (array) $incoming['product_context_records'] : array(),
            12,
            array( 'tool', 'product_id', 'recorded_at', 'label', 'query' )
        );
        $session['media_context_records'] = $this->merge_session_list(
            isset( $current['media_context_records'] ) ? (array) $current['media_context_records'] : array(),
            isset( $incoming['media_context_records'] ) ? (array) $incoming['media_context_records'] : array(),
            6,
            array( 'attachment_image_attachment_id', 'attachment_image_attachment_ids', 'attachment_image_url', 'attachment_image_urls', 'message_id', 'recorded_at' )
        );
        $session['active_tools'] = $this->merge_active_tools(
            isset( $current['active_tools'] ) ? (array) $current['active_tools'] : array(),
            isset( $incoming['active_tools'] ) ? (array) $incoming['active_tools'] : array()
        );
        $session['extension_workflow'] = $this->choose_latest_extension_workflow(
            isset( $current['extension_workflow'] ) ? (array) $current['extension_workflow'] : array(),
            isset( $incoming['extension_workflow'] ) ? (array) $incoming['extension_workflow'] : array()
        );
        $session['task_attempt_budget'] = $this->choose_latest_task_attempt_budget(
            isset( $current['task_attempt_budget'] ) ? (array) $current['task_attempt_budget'] : array(),
            isset( $incoming['task_attempt_budget'] ) ? (array) $incoming['task_attempt_budget'] : array()
        );
        $session['turns'] = $this->merge_session_turns(
            isset( $current['turns'] ) ? (array) $current['turns'] : array(),
            isset( $incoming['turns'] ) ? (array) $incoming['turns'] : array()
        );

        $latest_turn = $this->find_latest_turn_record( $session['turns'] );
        if ( ! empty( $latest_turn['id'] ) ) {
            $session['last_turn_id'] = (string) $latest_turn['id'];
        } else {
            $session['last_turn_id'] = ! empty( $incoming['last_turn_id'] ) ? (string) $incoming['last_turn_id'] : ( isset( $current['last_turn_id'] ) ? (string) $current['last_turn_id'] : '' );
        }

        return $this->normalize_session_record( $session_id, $session );
    }

    private function merge_session_turns( $current_turns, $incoming_turns ) {
        $merged = array();

        foreach ( array_merge( (array) $current_turns, (array) $incoming_turns ) as $turn ) {
            if ( ! is_array( $turn ) ) {
                continue;
            }

            $turn_id = isset( $turn['id'] ) ? trim( (string) $turn['id'] ) : '';
            if ( '' === $turn_id ) {
                $turn_id = md5( wp_json_encode( $turn ) );
            }

            if ( ! isset( $merged[ $turn_id ] ) ) {
                $merged[ $turn_id ] = $turn;
                continue;
            }

            $existing                  = $merged[ $turn_id ];
            $merged_turn               = array_merge( $existing, $turn );
            $merged_turn['prompt']     = $this->prefer_longer_text( isset( $existing['prompt'] ) ? (string) $existing['prompt'] : '', isset( $turn['prompt'] ) ? (string) $turn['prompt'] : '' );
            $merged_turn['reply']      = $this->prefer_longer_text( isset( $existing['reply'] ) ? (string) $existing['reply'] : '', isset( $turn['reply'] ) ? (string) $turn['reply'] : '' );
            $merged_turn['status']     = $this->prefer_turn_status( isset( $existing['status'] ) ? (string) $existing['status'] : '', isset( $turn['status'] ) ? (string) $turn['status'] : '' );
            $merged_turn['started_at'] = $this->merge_min_positive_int( isset( $existing['started_at'] ) ? $existing['started_at'] : 0, isset( $turn['started_at'] ) ? $turn['started_at'] : 0 );
            $merged_turn['ended_at']   = max( intval( isset( $existing['ended_at'] ) ? $existing['ended_at'] : 0 ), intval( isset( $turn['ended_at'] ) ? $turn['ended_at'] : 0 ) );
            $merged_turn['meta']       = array_merge(
                isset( $existing['meta'] ) && is_array( $existing['meta'] ) ? $existing['meta'] : array(),
                isset( $turn['meta'] ) && is_array( $turn['meta'] ) ? $turn['meta'] : array()
            );
            $merged_turn['tool_events'] = $this->merge_session_list(
                isset( $existing['tool_events'] ) ? (array) $existing['tool_events'] : array(),
                isset( $turn['tool_events'] ) ? (array) $turn['tool_events'] : array(),
                self::MAX_TOOL_EVENTS_PER_TURN,
                array( 'name', 'recorded_at', 'args', 'result' )
            );
            $merged[ $turn_id ] = $merged_turn;
        }

        $turns = array_values( $merged );
        usort(
            $turns,
            function ( $a, $b ) {
                $a_time = intval( isset( $a['started_at'] ) ? $a['started_at'] : ( isset( $a['ended_at'] ) ? $a['ended_at'] : 0 ) );
                $b_time = intval( isset( $b['started_at'] ) ? $b['started_at'] : ( isset( $b['ended_at'] ) ? $b['ended_at'] : 0 ) );

                if ( $a_time === $b_time ) {
                    return strcmp(
                        isset( $a['id'] ) ? (string) $a['id'] : '',
                        isset( $b['id'] ) ? (string) $b['id'] : ''
                    );
                }

                return $a_time <=> $b_time;
            }
        );

        return array_slice( $turns, -self::MAX_RECENT_TURNS );
    }

    private function merge_session_list( $current_items, $incoming_items, $limit, $identity_fields ) {
        $merged = array();

        foreach ( array_merge( (array) $current_items, (array) $incoming_items ) as $item ) {
            if ( ! is_array( $item ) ) {
                continue;
            }

            $key = $this->build_session_list_identity( $item, $identity_fields );
            if ( isset( $merged[ $key ] ) ) {
                $merged[ $key ] = array_merge( $merged[ $key ], $item );
            } else {
                $merged[ $key ] = $item;
            }
        }

        $items = array_values( $merged );
        usort(
            $items,
            function ( $a, $b ) {
                return $this->extract_session_list_timestamp( $a ) <=> $this->extract_session_list_timestamp( $b );
            }
        );

        return array_slice( $items, -max( 1, intval( $limit ) ) );
    }

    private function build_session_list_identity( $item, $identity_fields ) {
        $item  = is_array( $item ) ? $item : array();
        $parts = array();
        foreach ( (array) $identity_fields as $field ) {
            if ( array_key_exists( $field, $item ) ) {
                $value = $item[ $field ];
                if ( is_array( $value ) ) {
                    $parts[] = wp_json_encode( $value );
                } else {
                    $parts[] = (string) $value;
                }
            }
        }

        if ( empty( $parts ) ) {
            $parts[] = wp_json_encode( $item );
        }

        return md5( implode( '|', $parts ) );
    }

    private function extract_session_list_timestamp( $item ) {
        foreach ( array( 'recorded_at', 'ts', 'updated_at', 'loaded_at', 'started_at', 'ended_at' ) as $field ) {
            if ( ! empty( $item[ $field ] ) ) {
                return intval( $item[ $field ] );
            }
        }

        return 0;
    }

    private function merge_active_tools( $current_tools, $incoming_tools ) {
        $merged = $this->normalize_active_tools_map( (array) $current_tools );

        foreach ( $this->normalize_active_tools_map( (array) $incoming_tools ) as $name => $record ) {
            $existing = isset( $merged[ $name ] ) && is_array( $merged[ $name ] ) ? $merged[ $name ] : array();
            $merged[ $name ] = array(
                'loaded_at'    => $this->merge_min_positive_int( isset( $existing['loaded_at'] ) ? $existing['loaded_at'] : 0, isset( $record['loaded_at'] ) ? $record['loaded_at'] : 0 ),
                'last_used_at' => max( intval( isset( $existing['last_used_at'] ) ? $existing['last_used_at'] : 0 ), intval( isset( $record['last_used_at'] ) ? $record['last_used_at'] : 0 ) ),
                'expires_at'   => max( intval( isset( $existing['expires_at'] ) ? $existing['expires_at'] : 0 ), intval( isset( $record['expires_at'] ) ? $record['expires_at'] : 0 ) ),
                'turn_id'      => ! empty( $record['turn_id'] ) ? sanitize_text_field( (string) $record['turn_id'] ) : ( isset( $existing['turn_id'] ) ? sanitize_text_field( (string) $existing['turn_id'] ) : '' ),
            );
        }

        return $this->normalize_active_tools_map( $merged );
    }

    private function find_latest_turn_record( $turns ) {
        $latest      = array();
        $latest_time = 0;

        foreach ( (array) $turns as $turn ) {
            if ( ! is_array( $turn ) ) {
                continue;
            }

            $time = max( intval( isset( $turn['ended_at'] ) ? $turn['ended_at'] : 0 ), intval( isset( $turn['started_at'] ) ? $turn['started_at'] : 0 ) );
            if ( $time >= $latest_time ) {
                $latest      = $turn;
                $latest_time = $time;
            }
        }

        return $latest;
    }

    private function prefer_longer_text( $current, $incoming ) {
        $current  = trim( (string) $current );
        $incoming = trim( (string) $incoming );

        if ( '' === $current ) {
            return $incoming;
        }
        if ( '' === $incoming ) {
            return $current;
        }

        return strlen( $incoming ) >= strlen( $current ) ? $incoming : $current;
    }

    private function prefer_turn_status( $current, $incoming ) {
        $rank = array(
            'running'   => 1,
            'queued'    => 2,
            'cancelled' => 3,
            'failed'    => 4,
            'recovered' => 5,
            'completed' => 6,
        );

        $current_rank  = isset( $rank[ $current ] ) ? intval( $rank[ $current ] ) : 0;
        $incoming_rank = isset( $rank[ $incoming ] ) ? intval( $rank[ $incoming ] ) : 0;

        return $incoming_rank >= $current_rank ? $incoming : $current;
    }

    private function merge_min_positive_int( $left, $right ) {
        $left  = intval( $left );
        $right = intval( $right );

        if ( $left < 1 ) {
            return max( 0, $right );
        }
        if ( $right < 1 ) {
            return max( 0, $left );
        }

        return min( $left, $right );
    }

    private function build_session_manifest( $session_id, $session ) {
        return array(
            'session_id'       => (string) $session_id,
            'updated_at'       => intval( isset( $session['updated_at'] ) ? $session['updated_at'] : 0 ),
            'mode'             => isset( $session['mode'] ) ? (string) $session['mode'] : 'main',
            'turn_count'       => intval( isset( $session['turn_count'] ) ? $session['turn_count'] : 0 ),
            'compaction_count' => intval( isset( $session['compaction_count'] ) ? $session['compaction_count'] : 0 ),
            'session_label'    => isset( $session['session_label'] ) ? (string) $session['session_label'] : '',
            'session_scope'    => isset( $session['session_scope'] ) ? (string) $session['session_scope'] : '',
            'subagent_depth'   => max( 0, intval( isset( $session['subagent_depth'] ) ? $session['subagent_depth'] : 0 ) ),
            'allow_writes'     => ! empty( $session['allow_writes'] ),
            'parent_session_id' => isset( $session['parent_session_id'] ) ? (string) $session['parent_session_id'] : '',
            'parent_turn_id'   => isset( $session['parent_turn_id'] ) ? (string) $session['parent_turn_id'] : '',
            'last_turn_id'     => isset( $session['last_turn_id'] ) ? (string) $session['last_turn_id'] : '',
            'session_file'     => isset( $session['session_file'] ) && '' !== trim( (string) $session['session_file'] ) ? (string) $session['session_file'] : $this->get_transcript_path( $session_id ),
            'worklog_summary'  => isset( $session['worklog_summary'] ) ? (string) $session['worklog_summary'] : '',
            'session_ram'      => isset( $session['session_ram'] ) && is_array( $session['session_ram'] ) ? $session['session_ram'] : $this->get_default_session_ram(),
            'sticky_notes'     => isset( $session['sticky_notes'] ) ? array_values( (array) $session['sticky_notes'] ) : array(),
            'recent_subagents' => isset( $session['recent_subagents'] ) ? array_values( (array) $session['recent_subagents'] ) : array(),
            'recent_turns'     => isset( $session['turns'] ) ? array_values( (array) $session['turns'] ) : array(),
            'product_context_records' => isset( $session['product_context_records'] ) ? array_values( (array) $session['product_context_records'] ) : array(),
            'media_context_records' => isset( $session['media_context_records'] ) ? array_values( (array) $session['media_context_records'] ) : array(),
            'active_tools'     => isset( $session['active_tools'] ) ? $session['active_tools'] : array(),
            'extension_workflow' => isset( $session['extension_workflow'] ) && is_array( $session['extension_workflow'] ) ? $this->normalize_extension_workflow_state( $session['extension_workflow'] ) : array(),
            'task_attempt_budget' => isset( $session['task_attempt_budget'] ) && is_array( $session['task_attempt_budget'] ) ? $this->normalize_task_attempt_budget_state( $session['task_attempt_budget'] ) : array(),
        );
    }

    private function append_transcript_entry( $session_id, $entry ) {
        if ( ! $this->workspace || ! method_exists( $this->workspace, 'append_session_transcript' ) ) {
            return;
        }

        $entry = is_array( $entry ) ? $entry : array();
        if ( $this->is_internal_history_record( $entry ) ) {
            return;
        }
        if ( empty( $entry['ts'] ) ) {
            $entry['ts'] = time();
        }
        $this->workspace->append_session_transcript( $session_id, $entry );
    }

    private function is_internal_history_record( $record ) {
        if ( ! is_array( $record ) ) {
            return false;
        }

        $meta = isset( $record['meta'] ) && is_array( $record['meta'] ) ? $record['meta'] : array();
        if ( empty( $meta ) ) {
            return false;
        }

        $visibility = isset( $meta['visibility'] ) ? sanitize_key( (string) $meta['visibility'] ) : '';
        if ( 'internal' === $visibility || 'hidden' === $visibility ) {
            return true;
        }

        $source = isset( $meta['source'] ) ? sanitize_key( (string) $meta['source'] ) : '';
        if ( in_array( $source, array( 'scheduled_self_wake', 'scheduled_job', 'heartbeat', 'proactive_task', 'autonomous_task' ), true ) ) {
            return true;
        }

        $channel = isset( $meta['channel'] ) ? sanitize_key( (string) $meta['channel'] ) : '';
        if ( 'scheduled' === $channel ) {
            return true;
        }

        if ( array_key_exists( 'internal', $meta ) ) {
            $internal = $meta['internal'];
            return true === $internal || 'true' === strtolower( (string) $internal ) || '1' === (string) $internal;
        }

        return false;
    }

    /**
     * True for transcript records that were pushed into the chat by an
     * automated delivery (scheduled job, operative heartbeat) rather than
     * produced as a reply inside the live conversation.
     */
    private function is_automated_delivery_record( $record ) {
        if ( ! is_array( $record ) ) {
            return false;
        }

        $meta = isset( $record['meta'] ) && is_array( $record['meta'] ) ? $record['meta'] : array();
        if ( empty( $meta ) ) {
            return false;
        }

        $source = isset( $meta['source'] ) ? sanitize_key( (string) $meta['source'] ) : '';
        if ( in_array( $source, array( 'scheduled_job', 'operative_heartbeat', 'scheduled_self_wake', 'heartbeat', 'proactive_task', 'autonomous_task' ), true ) ) {
            return true;
        }

        foreach ( array( 'is_heartbeat', 'is_operative_heartbeat' ) as $flag ) {
            if ( ! array_key_exists( $flag, $meta ) ) {
                continue;
            }
            $value = $meta[ $flag ];
            if ( true === $value || 'true' === strtolower( (string) $value ) || '1' === (string) $value ) {
                return true;
            }
        }

        return false;
    }

    private function get_transcript_path( $session_id ) {
        if ( ! $this->workspace || ! method_exists( $this->workspace, 'get_session_transcript_path' ) ) {
            return '';
        }

        return (string) $this->workspace->get_session_transcript_path( $session_id );
    }

    private function find_turn_index( $session, $turn_id ) {
        $turns = isset( $session['turns'] ) && is_array( $session['turns'] ) ? $session['turns'] : array();
        foreach ( $turns as $index => $turn ) {
            if ( isset( $turn['id'] ) && (string) $turn['id'] === $turn_id ) {
                return $index;
            }
        }

        return null;
    }

    private function generate_turn_id() {
        return function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'turn_', true );
    }

    private function session_lock_name( $session_id ) {
        return 'session-' . md5( (string) $session_id );
    }

    private function truncate_text( $text, $limit ) {
        $text  = trim( (string) $text );
        $limit = max( 32, intval( $limit ) );
        $len   = function_exists( 'mb_strlen' ) ? mb_strlen( $text, 'UTF-8' ) : strlen( $text );
        if ( '' === $text || $len <= $limit ) {
            return $text;
        }

        $cut = function_exists( 'mb_substr' ) ? mb_substr( $text, 0, $limit - 3, 'UTF-8' ) : substr( $text, 0, $limit - 3 );
        return $cut . '...';
    }

    private function encode_compact_json( $value ) {
        if ( is_scalar( $value ) || null === $value ) {
            return (string) $value;
        }

        $encoded = wp_json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
        return is_string( $encoded ) ? $encoded : '';
    }
}
