<?php

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

/**
 * Turn Engine - facade for the Agent Loop.
 *
 * Delegates all execution to Niwa_AI_Agent_Loop while preserving the original
 * public API so that callers (Admin_Autonomy_Chat,
 * Subagent_Manager, etc.) do not need changes.
 *
 * @package Niwa_AI
 * @since   0.10.31
 */
class Niwa_AI_Admin_Autonomy_Turn_Engine {
    const MAX_EXECUTION_CONTRACT_FOLLOW_UPS = 4;
    const MAX_TASK_RECOVERY_ATTEMPTS = 2;

    /** @var Niwa_AI_AI_Client_Interface */
    private $openai_client;
    private $session_state;
	private $execution_lane;

    /** @var Niwa_AI_Agent_Loop|null Lazily created. */
    private $agent_loop;

    /** @var callable|null Steering messages provider for mid-turn injection. */
    private $steering_messages_provider;

    /** @var callable|null Follow-up messages provider for post-turn continuation. */
    private $follow_up_messages_provider;

    /** @var string Session ID of the currently executing turn (empty when idle). */
    private $current_session_id = '';

	public function __construct( Niwa_AI_AI_Client_Interface $openai_client, $session_state = null, $execution_lane = null ) {
        $this->openai_client = $openai_client;
        $this->session_state = $session_state;
		$this->execution_lane = $execution_lane;
    }

    /**
     * Set the steering messages provider callback.
     *
     * The callback is invoked before each LLM API call inside the agent loop.
     * It should return an array of message arrays (role + content) or empty.
     *
     * @param callable $provider Steering messages provider.
     */
    public function set_steering_messages_provider( $provider ) {
        if ( is_callable( $provider ) ) {
            $this->steering_messages_provider = $provider;
        }
    }

    /**
     * Set the follow-up messages provider callback.
     *
     * @param callable $provider Follow-up messages provider.
     * @return void
     */
    public function set_follow_up_messages_provider( $provider ) {
        if ( is_callable( $provider ) ) {
            $this->follow_up_messages_provider = $provider;
        }
    }

    /**
     * Get the session ID of the currently executing turn.
     *
     * Returns empty string when no turn is running. Used by the steering
     * provider closure to know which session's buffer to drain.
     *
     * @return string
     */
    public function get_current_session_id() {
        return $this->current_session_id;
    }

    /**
     * Run a turn.
     *
     * Signature is 100% backward-compatible. Internally delegates to Agent Loop.
     *
     * @param object $bridge   Object with build_turn_runtime() and execute_turn_tool_calls().
     * @param string $prompt   User prompt.
     * @param array  $context  Context array (session_id, user_role, etc.).
     * @param array  $options  Options (mode, lock_ttl, cancel_probe, progress_callback, etc.).
     * @return array { success: bool, reply: string, model: string, turn_id: string, error: string }
     */
    public function run( $bridge, $prompt, $context = array(), $options = array() ) {
        $prompt     = trim( (string) $prompt );
        $context    = is_array( $context ) ? $context : array();
        $options    = is_array( $options ) ? $options : array();
        $mode       = isset( $options['mode'] ) ? sanitize_key( (string) $options['mode'] ) : 'main';
        $session_id = isset( $context['session_id'] ) ? (string) $context['session_id'] : '';
        $lock_ttl   = isset( $options['lock_ttl'] ) ? intval( $options['lock_ttl'] ) : 480;
        $cancel_probe      = isset( $options['cancel_probe'] ) && is_callable( $options['cancel_probe'] ) ? $options['cancel_probe'] : null;
        $progress_callback = isset( $options['progress_callback'] ) && is_callable( $options['progress_callback'] ) ? $options['progress_callback'] : null;
        $soft_deadline     = isset( $context['soft_deadline'] ) ? intval( $context['soft_deadline'] ) : 0;
        $channel           = isset( $context['channel'] ) ? sanitize_key( (string) $context['channel'] ) : '';
        $meta_options      = $options;
        unset( $meta_options['cancel_probe'], $meta_options['progress_callback'] );
        $meta_options['allow_writes'] = 'subagent' === $mode ? ! empty( $options['allow_writes'] ) : true;
        if (
            ! empty( $context['is_heartbeat'] )
            || ! empty( $context['is_operative_heartbeat'] )
            || ! empty( $context['is_proactive_task'] )
            || ! empty( $context['scheduled_self_wake_execution'] )
            || ! empty( $context['scheduled_job'] )
            || in_array( isset( $context['source'] ) ? sanitize_key( (string) $context['source'] ) : '', array( 'scheduled_self_wake', 'scheduled_job', 'heartbeat', 'proactive_task', 'autonomous_task' ), true )
            || ! empty( $options['internal'] )
        ) {
            $meta_options['internal']   = true;
            $meta_options['visibility'] = 'internal';
            $meta_options['source']     = isset( $context['source'] ) ? sanitize_key( (string) $context['source'] ) : ( isset( $meta_options['source'] ) ? sanitize_key( (string) $meta_options['source'] ) : '' );
        }

        if ( '' === $prompt ) {
            return array(
                'success' => false,
                'error'   => 'Prazna poruka.',
            );
        }

        if ( ! $this->openai_client ) {
            return array(
                'success' => false,
                'error'   => 'Turn engine nema AI client.',
            );
        }

        if ( ! is_object( $bridge ) || ! method_exists( $bridge, 'build_turn_runtime' ) || ! method_exists( $bridge, 'execute_turn_tool_calls' ) ) {
            return array(
                'success' => false,
                'error'   => 'Turn bridge nije validan.',
            );
        }

        $turn    = $this->session_state ? $this->session_state->begin_turn( $session_id, $mode, $prompt, $meta_options ) : array( 'id' => '' );
        $turn_id = isset( $turn['id'] ) ? trim( (string) $turn['id'] ) : '';
        if ( '' !== $turn_id ) {
            $context['turn_id'] = $turn_id;
            $options['turn_id'] = $turn_id;
            $meta_options['turn_id'] = $turn_id;
        }
        $this->current_session_id = $session_id;

        try {
            if ( $this->should_cancel_turn( $cancel_probe ) ) {
                $error = 'Turn cancelled before model start.';
                $this->finish_turn( $session_id, $turn, '', 'cancelled', array( 'error' => $error ) );
                return array(
                    'success'   => false,
                    'cancelled' => true,
                    'error'     => $error,
                );
            }

            $wrapped_progress_callback = $this->wrap_progress_callback( $session_id, $lock_ttl, $progress_callback );

            // Wrap cancel_probe to also check the soft deadline.
            $effective_cancel_probe = $cancel_probe;
            if ( $soft_deadline > 0 ) {
                $effective_cancel_probe = function () use ( $cancel_probe, $soft_deadline ) {
                    if ( is_callable( $cancel_probe ) && ! empty( call_user_func( $cancel_probe ) ) ) {
                        return true;
                    }
                    return time() >= $soft_deadline;
                };
            }

            $continuation_provenance      = $this->resolve_continuation_execution_provenance( $context );
            $executed_tools               = $continuation_provenance['executed_tools'];
            $loaded_tools_detected        = false;
            $successful_write_execution   = $continuation_provenance['successful_write_execution'];
            $execution_follow_up_injected = false;
            $execution_contract_follow_up_count = 0;
            $pending_execution_follow_up  = array();
            $runtime_reload_follow_up     = array();
            $scheduled_delivery_tool_successful = false;
            $scheduled_delivery_child_scheduled = false;
            $scheduled_delivery_prepared_message = '';
            $subagent_dispatch_successful = false;
            $pending_subagent_jobs = array();
            $extension_approval_message = '';
            $task_checkpoint             = array();
            $execution_contract           = $this->initialize_execution_contract();
            $runtime_policy               = is_object( $bridge ) && method_exists( $bridge, 'resolve_turn_runtime_policy' )
                ? (array) $bridge->resolve_turn_runtime_policy( $context, $mode, $options )
                : array();
            $enforce_tool_chain_contract  = array_key_exists( 'enforce_execution_contract', $runtime_policy )
                ? ! empty( $runtime_policy['enforce_execution_contract'] )
                : $this->should_enforce_execution_contract_for_turn( $context, $options );
            $allow_execution_follow_up    = array_key_exists( 'allow_execution_follow_up', $runtime_policy )
                ? ! empty( $runtime_policy['allow_execution_follow_up'] )
                : $enforce_tool_chain_contract;
            $require_task_checkpoint      = $this->should_require_task_checkpoint_for_turn( $context, $mode, $options );
            if (
                $require_task_checkpoint
                && 'task_checkpoint_continue' === sanitize_key( (string) ( $context['continuation_context']['continuation_reason'] ?? '' ) )
            ) {
                $execution_contract = $this->add_execution_contract_requirement(
                    $execution_contract,
                    'task_checkpoint',
                    'Finish the resumed task with task_checkpoint after completing the recorded remaining work.',
                    'continuation',
                    array(
                        'kind' => 'task_checkpoint',
                    )
                );
            }
            $existing_follow_up_provider  = $this->follow_up_messages_provider;
            $after_tool_call = function ( $call, $tool_payload, $agent_context ) use ( $bridge, $context, $mode, $options, $enforce_tool_chain_contract, $allow_execution_follow_up, $require_task_checkpoint, &$executed_tools, &$loaded_tools_detected, &$successful_write_execution, &$pending_execution_follow_up, &$runtime_reload_follow_up, &$scheduled_delivery_tool_successful, &$scheduled_delivery_child_scheduled, &$scheduled_delivery_prepared_message, &$subagent_dispatch_successful, &$pending_subagent_jobs, &$extension_approval_message, &$task_checkpoint, &$execution_contract ) {
                $tool_name = is_array( $call ) && isset( $call['name'] ) ? sanitize_key( (string) $call['name'] ) : '';

                $decoded_payload = $this->decode_tool_payload_content( $tool_payload );
                if ( '' !== $tool_name ) {
                    $executed_tools[] = $tool_name;
                }
                $tool_succeeded  = $this->did_tool_payload_succeed( $decoded_payload );
                $is_write_tool   = $this->is_verified_write_execution( $tool_name, $decoded_payload, $bridge );
                if (
                    class_exists( 'Niwa_AI_Admin_Autonomy_Task_Integrity' )
                    && is_object( $agent_context )
                    && method_exists( $agent_context, 'get_tool_evidence' )
                    && method_exists( $agent_context, 'set_tool_evidence' )
                ) {
                    $agent_context->set_tool_evidence(
                        Niwa_AI_Admin_Autonomy_Task_Integrity::append_tool_evidence(
                            $agent_context->get_tool_evidence(),
                            $call,
                            $decoded_payload,
                            $tool_succeeded,
                            $is_write_tool
                        )
                    );
                    if ( method_exists( $agent_context, 'record_execution_provenance' ) ) {
                        $evidence = $agent_context->get_tool_evidence();
                        $last_evidence = ! empty( $evidence ) ? end( $evidence ) : array();
                        $agent_context->record_execution_provenance(
                            $tool_name,
                            $tool_succeeded,
                            $is_write_tool,
                            isset( $last_evidence['target'] ) && is_array( $last_evidence['target'] ) ? $last_evidence['target'] : array()
                        );
                    }
                }
                if ( 'task_checkpoint' === $tool_name && $tool_succeeded ) {
                    $task_checkpoint = $decoded_payload;
                }
                $execution_contract = $this->update_execution_contract_from_tool_call( $execution_contract, $tool_name, $decoded_payload, $tool_succeeded, $enforce_tool_chain_contract, $bridge, $require_task_checkpoint );
                $is_scheduled_delivery_preparation = ! empty( $context['scheduled_research_delivery'] )
                    && isset( $context['scheduled_delivery_phase'] )
                    && 'preparation' === (string) $context['scheduled_delivery_phase'];
                if ( $tool_succeeded && 'send_admin_delivery_message' === $tool_name && ! $is_scheduled_delivery_preparation ) {
                    $scheduled_delivery_tool_successful = true;
                }
                if ( $tool_succeeded && 'send_admin_delivery_message' === $tool_name && $is_scheduled_delivery_preparation && ! empty( $decoded_payload['prepared_delivery_message'] ) ) {
                    $scheduled_delivery_prepared_message = trim( (string) $decoded_payload['prepared_delivery_message'] );
                }
                if (
                    $tool_succeeded
                    && $is_scheduled_delivery_preparation
                    && in_array( $tool_name, array( 'schedule_reminder', 'schedule_self_wake' ), true )
                ) {
                    $scheduled_delivery_child_scheduled = true;
                }
                if ( $tool_succeeded && in_array( $tool_name, array( 'subagent_spawn', 'sessions_spawn', 'sessions_send', 'coding_workflow_start', 'schedule_self_wake' ), true ) ) {
                    $job = isset( $decoded_payload['job'] ) && is_array( $decoded_payload['job'] ) ? $decoded_payload['job'] : array();
                    $job_id = isset( $decoded_payload['job_id'] ) ? (string) $decoded_payload['job_id'] : ( isset( $job['id'] ) ? (string) $job['id'] : '' );
                    $key = '' !== $job_id ? $tool_name . ':' . $job_id : $tool_name . ':' . count( $pending_subagent_jobs );
                    $pending_subagent_jobs[ $key ] = array(
                        'tool'       => $tool_name,
                        'job_id'     => $job_id,
                        'session_id' => isset( $decoded_payload['session_id'] ) ? (string) $decoded_payload['session_id'] : '',
                        'status'     => isset( $decoded_payload['status'] ) ? sanitize_key( (string) $decoded_payload['status'] ) : ( isset( $job['status'] ) ? sanitize_key( (string) $job['status'] ) : 'queued' ),
                    );
                    $subagent_dispatch_successful = true;
                }
                if ( $tool_succeeded && 'extension_request_create' === $tool_name && empty( $decoded_payload['skipped'] ) && ! empty( $decoded_payload['safe_user_message'] ) ) {
                    $extension_approval_message = trim( (string) $decoded_payload['safe_user_message'] );
                }

                if ( ! empty( $enforce_tool_chain_contract ) && 'tool_load' === $tool_name && $tool_succeeded && ! empty( $decoded_payload['loaded'] ) && is_array( $decoded_payload['loaded'] ) ) {
                    $loaded_tools_detected = true;
                    if ( ! $successful_write_execution ) {
                        $pending_execution_follow_up = array(
                            'role'    => 'system',
                            'content' => '[CONTINUE EXECUTION] You already loaded the required tools in this turn. Continue with the founder\'s requested action now. Do not stop with a status update, promise, or extra confirmation unless a specific identifier is still missing.',
                        );
                    }
                }

                if ( ! empty( $allow_execution_follow_up ) && 'tool_search' === $tool_name && $tool_succeeded && ! empty( $decoded_payload['capability_gap'] ) && ! empty( $decoded_payload['extension_allowed'] ) && empty( $decoded_payload['native_ready'] ) && ! $successful_write_execution ) {
                    $missing_capability = ! empty( $decoded_payload['missing_capability'] ) ? sanitize_key( (string) $decoded_payload['missing_capability'] ) : 'runtime_capability_gap';
                    $pending_execution_follow_up = array(
                        'role'    => 'system',
                        'content' => '[CAPABILITY GAP ROUTER] tool_search did not find a native tool that confidently covers the founder task. First use an already-available generic read-only runtime inspection tool when it can answer the one-time request from local WordPress, database, filesystem, or logs without changing code. Otherwise call extension_request_create with missing_capability="' . $missing_capability . '", include the original task and search evidence, then ask the founder for approval.',
                    );
                } elseif ( ! empty( $allow_execution_follow_up ) && 'tool_search' === $tool_name && $tool_succeeded && ! empty( $decoded_payload['auto_loaded'] ) && is_array( $decoded_payload['auto_loaded'] ) && ! $successful_write_execution ) {
                    $loaded_names = array_values(
                        array_filter(
                            array_map(
                                function ( $name ) {
                                    return sanitize_key( (string) $name );
                                },
                                $decoded_payload['auto_loaded']
                            )
                        )
                    );
                    $pending_execution_follow_up = array(
                        'role'    => 'system',
                        'content' => '[TOOL SEARCH AUTO-LOADED] tool_search already activated the matching tools' . ( ! empty( $loaded_names ) ? ': ' . implode( ', ', $loaded_names ) : '' ) . '. Call the needed tool directly now. Do not call tool_load or repeat tool_search for this task.',
                    );
                } elseif ( ! empty( $allow_execution_follow_up ) && 'tool_search' === $tool_name && $tool_succeeded && ! empty( $decoded_payload['native_ready'] ) && ! empty( $decoded_payload['native_tool_name'] ) && ! $successful_write_execution ) {
                    $pending_execution_follow_up = array(
                        'role'    => 'system',
                        'content' => '[NATIVE TOOL SEARCH READY] tool_search found native tool ' . sanitize_key( (string) $decoded_payload['native_tool_name'] ) . '. It is already available in this run. Call it directly now; do not call tool_load or repeat tool_search for this task.',
                    );
                } elseif ( ! empty( $allow_execution_follow_up ) && 'tool_search' === $tool_name && $tool_succeeded && ! empty( $decoded_payload['results'] ) && is_array( $decoded_payload['results'] ) && ! $successful_write_execution ) {
                    $candidate_names = array_values(
                        array_filter(
                            array_map(
                                function ( $row ) {
                                    if ( is_array( $row ) && ( ! empty( $row['native'] ) || 'native' === ( isset( $row['entry_type'] ) ? (string) $row['entry_type'] : '' ) ) ) {
                                        return '';
                                    }
                                    return is_array( $row ) && ! empty( $row['name'] ) ? sanitize_key( (string) $row['name'] ) : '';
                                },
                                array_slice( $decoded_payload['results'], 0, 4 )
                            )
                        )
                    );
                    if ( empty( $candidate_names ) ) {
                        $pending_execution_follow_up = array();
                    } else {
                        $pending_execution_follow_up = array(
                            'role'    => 'system',
                            'content' => '[TOOL SEARCH RESOLVED] tool_search already returned matching tools: ' . implode( ', ', $candidate_names ) . '. Call tool_load with one or more exact tool names now instead of searching again, unless the search results are clearly irrelevant to the founder\'s request.',
                        );
                    }
                }

                if ( $is_write_tool && $tool_succeeded ) {
                    $successful_write_execution  = true;
                    $pending_execution_follow_up = array();
                    if ( is_object( $agent_context ) && method_exists( $agent_context, 'get_api_options' ) && method_exists( $agent_context, 'set_api_options' ) ) {
                        $api_options                = (array) $agent_context->get_api_options();
                        $api_options['tool_choice'] = 'auto';
                        $agent_context->set_api_options( $api_options );
                    }
                    if ( ! empty( $decoded_payload['runtime_reload_required'] ) ) {
                        $runtime_reload_follow_up = array(
                            'role'    => 'system',
                            'content' => '[RUNTIME EXTENSION STAGED] You changed local PHP runtime files in this turn. The new or changed capability becomes available on the next request, not later in this same request. Do not claim the new tool is already live now. Tell the founder you performed a runtime extension, briefly say what capability changed, and say the task should continue on the next turn or heartbeat.',
                        );
                    }
                } elseif ( ! empty( $allow_execution_follow_up ) && ! empty( $decoded_payload['fallback_tool'] ) && ! $successful_write_execution ) {
                    $pending_execution_follow_up = array(
                        'role'    => 'system',
                        'content' => '[TOOL FALLBACK] The previous tool failed. Call ' . sanitize_key( (string) $decoded_payload['fallback_tool'] ) . ' now in this same turn. Do not ask for another confirmation. After execution, reply only with verified tool output.',
                    );
                }

                if ( is_object( $bridge ) && method_exists( $bridge, 'sync_runtime_tools_after_tool_call' ) ) {
                    $bridge->sync_runtime_tools_after_tool_call( $call, $tool_payload, $agent_context, $context, $mode, $options );
                }
            };
            $follow_up_messages_provider = function () use ( &$pending_execution_follow_up, &$runtime_reload_follow_up, &$successful_write_execution, &$execution_follow_up_injected, &$execution_contract_follow_up_count, &$execution_contract, $existing_follow_up_provider ) {
                $messages = array();

                if ( is_callable( $existing_follow_up_provider ) ) {
                    $provided = call_user_func( $existing_follow_up_provider );
                    if ( is_array( $provided ) ) {
                        $messages = $provided;
                    }
                }

                if ( $this->execution_contract_has_open_requirements( $execution_contract ) ) {
                    if ( $execution_contract_follow_up_count < self::MAX_EXECUTION_CONTRACT_FOLLOW_UPS ) {
                        $messages[] = $this->build_execution_contract_follow_up_message( $execution_contract );
                        $execution_contract_follow_up_count++;
                        $execution_follow_up_injected = true;
                    }
                } elseif ( ! $execution_follow_up_injected && ! $successful_write_execution && ! empty( $pending_execution_follow_up ) ) {
                    $messages[] = $pending_execution_follow_up;
                    $execution_follow_up_injected = true;
                }

                if ( ! empty( $runtime_reload_follow_up ) ) {
                    $messages[] = $runtime_reload_follow_up;
                    $runtime_reload_follow_up = array();
                }

                return $messages;
            };

			$loop = $this->get_agent_loop( array(
				'cancel_probe'               => $effective_cancel_probe,
				'progress_callback'          => $wrapped_progress_callback,
				'after_tool_call'            => $after_tool_call,
				'follow_up_messages_provider' => $follow_up_messages_provider,
				'max_iterations'             => $this->resolve_max_iterations( $mode, $options ),
			) );

            $result = $loop->run( $bridge, $prompt, $context, array_merge( $options, array(
                'turn_id'       => isset( $turn['id'] ) ? (string) $turn['id'] : '',
                'soft_deadline' => $soft_deadline,
            ) ) );
            $result = $this->apply_task_checkpoint_result( $result, $task_checkpoint, $execution_contract );

            $result['executed_tools']               = array_values( array_unique( array_filter( $executed_tools ) ) );
            $result['loaded_tools_detected']        = ! empty( $loaded_tools_detected );
            $result['successful_write_execution']   = ! empty( $successful_write_execution );
            if ( ! empty( $result['context_snapshot']['tool_evidence'] ) && is_array( $result['context_snapshot']['tool_evidence'] ) ) {
                $result['tool_evidence'] = array_slice( array_values( $result['context_snapshot']['tool_evidence'] ), -40 );
            }
            $result['execution_follow_up_injected'] = ! empty( $execution_follow_up_injected );
            $result['scheduled_delivery_tool_successful'] = ! empty( $scheduled_delivery_tool_successful );
            $result['scheduled_delivery_child_scheduled'] = ! empty( $scheduled_delivery_child_scheduled );
            if ( '' !== $scheduled_delivery_prepared_message ) {
                $result['prepared_delivery_message'] = $scheduled_delivery_prepared_message;
            }
            $result['execution_contract']           = $this->summarize_execution_contract( $execution_contract );
            $result['execution_contract_follow_ups'] = $execution_contract_follow_up_count;

            $result = $this->checkpoint_write_reconciliation( $context, $result, $successful_write_execution );

            if ( '' !== $extension_approval_message && empty( $result['cancelled'] ) && $this->has_pending_extension_workflow_for_session( $session_id, $prompt ) ) {
                $result['success']                    = true;
                $result['reply']                      = $extension_approval_message;
                $result['extension_pending_approval'] = true;
                $result['task_completion_state']      = 'pending_extension_approval';
                $result['original_task_completed']    = false;
                $result['retry_original_task']        = true;
            }

            if ( empty( $result['cancelled'] ) && $this->execution_contract_has_open_requirements( $execution_contract ) ) {
                if ( $this->result_is_extension_approval_ask( $result, $session_id ) ) {
                    // The turn ended by asking the founder to approve a
                    // self-extension. Blanking the reply would destroy the
                    // approval question and the founder would never be asked.
                    $result['extension_pending_approval'] = true;
                    $result['task_completion_state']      = 'pending_extension_approval';
                    $result['original_task_completed']    = false;
                    $result['execution_policy_warning']   = 'Execution contract left open items, but the turn is a self-extension approval ask; reply preserved for founder approval.';
                } else {
                    $result = $this->mark_result_execution_incomplete( $result, $execution_contract );
                }
            }

            if ( is_object( $bridge ) && method_exists( $bridge, 'enforce_turn_execution_policy' ) ) {
                $filtered = $bridge->enforce_turn_execution_policy( $prompt, $context, $options, $result );
                if ( is_array( $filtered ) ) {
                    $result = $filtered;
                }
            }

            $result = $this->maybe_run_task_adequacy_recovery(
                $bridge,
                $prompt,
                $context,
                array_merge(
                    $meta_options,
                    array(
                        'turn_id'       => isset( $turn['id'] ) ? (string) $turn['id'] : '',
                        'soft_deadline' => $soft_deadline,
                    )
                ),
                $mode,
                $result,
                $after_tool_call,
                $follow_up_messages_provider,
                $effective_cancel_probe,
                $wrapped_progress_callback,
                $soft_deadline,
                $session_id,
                $subagent_dispatch_successful,
                $pending_subagent_jobs,
                $executed_tools,
                $successful_write_execution,
                $execution_contract
            );

            if ( is_object( $bridge ) && method_exists( $bridge, 'enforce_turn_execution_policy' ) ) {
                $filtered = $bridge->enforce_turn_execution_policy( $prompt, $context, $options, $result );
                if ( is_array( $filtered ) ) {
                    $result = $filtered;
                }
            }

            if ( $this->has_pending_extension_workflow_for_session( $session_id, $prompt ) ) {
                $result['task_completion_state']    = 'pending_extension_approval';
                $result['original_task_completed']  = false;
                $result['extension_pending_approval'] = true;
                $result['retry_original_task']      = true;
            }

            $result['executed_tools']               = array_values( array_unique( array_filter( $executed_tools ) ) );
            $result['loaded_tools_detected']        = ! empty( $loaded_tools_detected );
            $result['successful_write_execution']   = ! empty( $successful_write_execution );
            if ( ! empty( $result['context_snapshot']['tool_evidence'] ) && is_array( $result['context_snapshot']['tool_evidence'] ) ) {
                $result['tool_evidence'] = array_slice( array_values( $result['context_snapshot']['tool_evidence'] ), -40 );
            }
            $result['execution_follow_up_injected'] = ! empty( $execution_follow_up_injected );
            $result['scheduled_delivery_tool_successful'] = ! empty( $scheduled_delivery_tool_successful );
            $result['scheduled_delivery_child_scheduled'] = ! empty( $scheduled_delivery_child_scheduled );
            $result['subagent_dispatch_successful'] = ! empty( $subagent_dispatch_successful );
            if ( ! empty( $pending_subagent_jobs ) ) {
                $result['pending_subagent_jobs'] = array_values( $pending_subagent_jobs );
            }
            if ( '' !== $scheduled_delivery_prepared_message ) {
                $result['prepared_delivery_message'] = $scheduled_delivery_prepared_message;
            }
            $result['execution_contract']           = $this->summarize_execution_contract( $execution_contract );
            $result['execution_contract_follow_ups'] = $execution_contract_follow_up_count;
            $this->record_prompt_cache_usage_snapshot( $result, $context, $channel, $mode );

            if ( ! empty( $result['success'] ) ) {
                $reply = isset( $result['reply'] ) ? (string) $result['reply'] : '';
                if ( class_exists( 'Niwa_AI_Humanizer' ) && '' !== trim( $reply ) ) {
                    $reply           = ( new Niwa_AI_Humanizer() )->apply( $reply );
                    $result['reply'] = $reply;
                }
                $model = isset( $result['model'] ) ? (string) $result['model'] : '';
                $finish_meta = array(
                    'channel'        => $channel,
                    'model'          => $model,
                    'iterations'     => isset( $result['iterations'] ) ? intval( $result['iterations'] ) : 0,
                    'executed_tools' => isset( $result['executed_tools'] ) && is_array( $result['executed_tools'] ) ? array_values( $result['executed_tools'] ) : array(),
                    'successful_write_execution' => ! empty( $result['successful_write_execution'] ),
                    'task_completion_state' => isset( $result['task_completion_state'] ) ? sanitize_key( (string) $result['task_completion_state'] ) : '',
                    'tool_evidence'  => isset( $result['tool_evidence'] ) && is_array( $result['tool_evidence'] ) ? $result['tool_evidence'] : array(),
                    'execution_contract' => isset( $result['execution_contract'] ) && is_array( $result['execution_contract'] ) ? $result['execution_contract'] : array(),
                );
                $finish_meta = $this->apply_internal_finish_meta( $finish_meta, $meta_options );
                $this->finish_turn( $session_id, $turn, $reply, 'completed', $finish_meta );
            } elseif ( ! empty( $result['cancelled'] ) ) {
                $finish_meta = array(
                    'channel'            => $channel,
                    'error'              => isset( $result['error'] ) ? (string) $result['error'] : 'Cancelled.',
                    'cancelled_reason'   => isset( $result['cancelled_reason'] ) ? (string) $result['cancelled_reason'] : '',
                    'model'              => isset( $result['model'] ) ? (string) $result['model'] : '',
                    'iterations'         => isset( $result['iterations'] ) ? intval( $result['iterations'] ) : 0,
                    'context_snapshot'   => isset( $result['context_snapshot'] ) && is_array( $result['context_snapshot'] ) ? $result['context_snapshot'] : array(),
                    'executed_tools'     => isset( $result['executed_tools'] ) && is_array( $result['executed_tools'] ) ? array_values( $result['executed_tools'] ) : array(),
                    'successful_write_execution' => ! empty( $result['successful_write_execution'] ),
                    'task_completion_state' => isset( $result['task_completion_state'] ) ? sanitize_key( (string) $result['task_completion_state'] ) : '',
                    'tool_evidence'      => isset( $result['tool_evidence'] ) && is_array( $result['tool_evidence'] ) ? $result['tool_evidence'] : array(),
                    'execution_contract' => isset( $result['execution_contract'] ) && is_array( $result['execution_contract'] ) ? $result['execution_contract'] : array(),
                );
                $finish_meta = $this->apply_internal_finish_meta( $finish_meta, $meta_options );
                $this->finish_turn( $session_id, $turn, '', 'cancelled', $finish_meta );
            } else {
                $finish_meta = array(
                    'channel'            => $channel,
                    'error'              => isset( $result['error'] ) ? (string) $result['error'] : 'Unknown error.',
                    'model'              => isset( $result['model'] ) ? (string) $result['model'] : '',
                    'iterations'         => isset( $result['iterations'] ) ? intval( $result['iterations'] ) : 0,
                    'context_snapshot'   => isset( $result['context_snapshot'] ) && is_array( $result['context_snapshot'] ) ? $result['context_snapshot'] : array(),
                    'executed_tools'     => isset( $result['executed_tools'] ) && is_array( $result['executed_tools'] ) ? array_values( $result['executed_tools'] ) : array(),
                    'successful_write_execution' => ! empty( $result['successful_write_execution'] ),
                    'task_completion_state' => isset( $result['task_completion_state'] ) ? sanitize_key( (string) $result['task_completion_state'] ) : '',
                    'tool_evidence'      => isset( $result['tool_evidence'] ) && is_array( $result['tool_evidence'] ) ? $result['tool_evidence'] : array(),
                    'execution_contract' => isset( $result['execution_contract'] ) && is_array( $result['execution_contract'] ) ? $result['execution_contract'] : array(),
                );
                $finish_meta = $this->apply_internal_finish_meta( $finish_meta, $meta_options );
                $this->finish_turn( $session_id, $turn, '', 'failed', $finish_meta );
            }

            if ( ! isset( $result['turn_id'] ) ) {
                $result['turn_id'] = isset( $turn['id'] ) ? (string) $turn['id'] : '';
            }

            return $result;
        } finally {
            $this->current_session_id = '';
        }
    }

    /**
     * Emergency release of the current session lane.
     *
     * Called from register_shutdown_function() when PHP hard-kills
     * (set_time_limit, OOM, fatal error). Prevents orphaned locks
     * that block all subsequent messages into the steering buffer.
     */
    public function release_current_session_lane() {
        $this->current_session_id = '';
    }

    private function maybe_run_task_adequacy_recovery( $bridge, $prompt, $context, $options, $mode, $result, $after_tool_call, $follow_up_messages_provider, $cancel_probe, $progress_callback, $soft_deadline, $session_id, &$subagent_dispatch_successful, &$pending_subagent_jobs, &$executed_tools, &$successful_write_execution, &$execution_contract ) {
        $result  = is_array( $result ) ? $result : array();
        $context = is_array( $context ) ? $context : array();
        $options = is_array( $options ) ? $options : array();
        $result  = $this->attach_runtime_execution_metadata(
            $result,
            $executed_tools,
            $successful_write_execution,
            $execution_contract
        );
        $task_identity = $this->resolve_task_attempt_identity( $prompt, $context, $options, $result );
        $result['task_integrity_id'] = $task_identity;
        if ( ! empty( $result['context_snapshot'] ) && is_array( $result['context_snapshot'] ) ) {
            $result['context_snapshot']['task_integrity_id'] = $task_identity;
        }
        if ( ! empty( $subagent_dispatch_successful ) ) {
            return $this->mark_subagent_handoff_pending( $session_id, $prompt, $result, $pending_subagent_jobs, $task_identity );
        }
        if ( ! $this->should_run_task_adequacy_reflect( $context, $options, $mode, $result ) ) {
            return $result;
        }

        $budget = $this->get_task_attempt_budget_for_prompt( $session_id, $prompt, $task_identity );
        $max_attempts = self::MAX_TASK_RECOVERY_ATTEMPTS;
        $attempt_records = isset( $result['task_adequacy_recovery_attempts'] ) && is_array( $result['task_adequacy_recovery_attempts'] )
            ? $result['task_adequacy_recovery_attempts']
            : array();

        for ( $guard = 0; $guard <= $max_attempts; $guard++ ) {
            if ( $this->should_cancel_turn( $cancel_probe ) ) {
                return $result;
            }

            $result = $this->attach_runtime_execution_metadata(
                $result,
                $executed_tools,
                $successful_write_execution,
                $execution_contract
            );
            $reflection = $this->task_adequacy_reflect( $bridge, $prompt, $context, $options, $result, $budget );
            $result['task_adequacy_reflection'] = $reflection;
            $status = $this->normalize_task_adequacy_status( isset( $reflection['status'] ) ? (string) $reflection['status'] : '' );

            if ( ! empty( $reflection['verification_unavailable'] ) ) {
                if ( ! $this->turn_has_integrity_sensitive_structure( $context, $options, $result ) ) {
                    $result['verification_unavailable'] = true;
                    return $result;
                }
                $verification_attempt = min(
                    $max_attempts,
                    max( 0, intval( $budget['attempt_count'] ?? 0 ) ) + 1
                );
                $budget = $this->update_task_attempt_budget_for_prompt(
                    $session_id,
                    $prompt,
                    array(
                        'status'          => 'verification_unavailable',
                        'attempt_count'   => $verification_attempt,
                        'last_blocker'    => 'task_integrity_verifier_unavailable',
                        'max_attempts'    => $max_attempts,
                        'attempted_paths' => array_merge(
                            isset( $budget['attempted_paths'] ) && is_array( $budget['attempted_paths'] ) ? $budget['attempted_paths'] : array(),
                            array( 'task_integrity_verifier_retry' )
                        ),
                    ),
                    $task_identity
                );
                if (
                    $verification_attempt < $max_attempts
                    && ! empty( $result['context_snapshot']['messages'] )
                    && is_array( $result['context_snapshot']['messages'] )
                    && class_exists( 'Niwa_AI_Admin_Autonomy_Task_Integrity' )
                ) {
                    $verification_context = Niwa_AI_Admin_Autonomy_Task_Integrity::build_recovery_context(
                        $context,
                        $result,
                        '[TASK INTEGRITY VERIFICATION CONTINUATION] The candidate reply was withheld because the integrity verifier was temporarily unavailable. Preserve the recorded tool evidence and do not repeat successful tool calls or writes. Reconcile a final reply from the saved chronological trace and structured evidence only. Do not claim work beyond that evidence.'
                    );
                    if ( ! empty( $verification_context['continuation_context'] ) && is_array( $verification_context['continuation_context'] ) ) {
                        $result['context_snapshot'] = $verification_context['continuation_context'];
                        $result['context_snapshot']['task_integrity_id'] = $task_identity;
                        $result['success']                 = false;
                        $result['cancelled']               = true;
                        $result['cancelled_reason']        = 'task_integrity_verification';
                        $result['reply']                   = '';
                        $result['error']                   = 'Task integrity verification is temporarily unavailable; continuing from the saved evidence without repeating writes.';
                        $result['task_completion_state']   = ! empty( $result['successful_write_execution'] )
                            ? 'write_done_verification_pending'
                            : 'verification_pending';
                        $result['original_task_completed'] = false;
                        $result['verification_unavailable'] = true;
                        return $result;
                    }
                }
                $result['success']                 = false;
                $result['reply']                   = '';
                $result['error']                   = 'Task integrity verification is unavailable; the unverified candidate reply was withheld.';
                $result['task_completion_state']   = ! empty( $result['successful_write_execution'] )
                    ? 'write_done_verification_unavailable'
                    : 'unverified';
                $result['original_task_completed'] = false;
                $result['verification_unavailable'] = true;
                return $result;
            }

            if ( 'adequate' === $status ) {
                $this->update_task_attempt_budget_for_prompt(
                    $session_id,
                    $prompt,
                    array(
                        'status'        => 'adequate',
                        'attempt_count' => 0,
                        'last_blocker'  => '',
                        'attempted_paths' => array(),
                        'max_attempts'  => $max_attempts,
                    ),
                    $task_identity
                );
                return $result;
            }

            if ( in_array( $status, array( 'unsafe_or_disallowed', 'not_feasible' ), true ) ) {
                $message = isset( $reflection['safe_user_message'] ) ? trim( (string) $reflection['safe_user_message'] ) : '';
                $this->update_task_attempt_budget_for_prompt(
                    $session_id,
                    $prompt,
                    array(
                        'status'        => $status,
                        'last_blocker'  => isset( $reflection['blocker'] ) ? (string) $reflection['blocker'] : ( isset( $reflection['reason'] ) ? (string) $reflection['reason'] : '' ),
                        'max_attempts'  => $max_attempts,
                    ),
                    $task_identity
                );
                if ( '' !== $message ) {
                    $result['success'] = true;
                    $result['reply']   = $message;
                }
                return $result;
            }

            if ( $this->has_pending_extension_workflow_for_session( $session_id, $prompt ) ) {
                $this->update_task_attempt_budget_for_prompt(
                    $session_id,
                    $prompt,
                    array(
                        'status'        => $status,
                        'last_blocker'  => isset( $reflection['blocker'] ) ? (string) $reflection['blocker'] : ( isset( $reflection['reason'] ) ? (string) $reflection['reason'] : '' ),
                        'max_attempts'  => $max_attempts,
                    ),
                    $task_identity
                );
                return $result;
            }

            $attempt_count = isset( $budget['attempt_count'] ) ? max( 0, intval( $budget['attempt_count'] ) ) : 0;
            if ( $attempt_count >= $max_attempts ) {
                return $this->block_task_after_two_attempts( $session_id, $prompt, $result, $budget, $reflection, $task_identity );
            }

            $attempt_count++;
            $attempted_paths = isset( $budget['attempted_paths'] ) && is_array( $budget['attempted_paths'] ) ? $budget['attempted_paths'] : array();
            $attempted_paths[] = $this->describe_task_adequacy_recovery_path( $reflection );
            $budget = $this->update_task_attempt_budget_for_prompt(
                $session_id,
                $prompt,
                array(
                    'attempt_count'   => $attempt_count,
                    'max_attempts'    => $max_attempts,
                    'status'          => 'recovering',
                    'last_blocker'    => isset( $reflection['blocker'] ) ? (string) $reflection['blocker'] : ( isset( $reflection['reason'] ) ? (string) $reflection['reason'] : '' ),
                    'attempted_paths' => $attempted_paths,
                ),
                $task_identity
            );

            $recovery_message = $this->build_agentic_growth_retry_context( $prompt, $result, $reflection, $budget );
            $retry_context = class_exists( 'Niwa_AI_Admin_Autonomy_Task_Integrity' )
                ? Niwa_AI_Admin_Autonomy_Task_Integrity::build_recovery_context( $context, $result, $recovery_message )
                : $context;
            $retry_context['agentic_growth_retry_context'] = $recovery_message;
            $retry_context['task_attempt_budget'] = $budget;
            $retry_options = array_merge(
                $options,
                array(
                    'task_adequacy_retry_attempt' => $attempt_count,
                    'max_iterations'              => max( 4, $this->resolve_max_iterations( $mode, $options ) ),
                )
            );
            if ( $soft_deadline > 0 ) {
                $retry_options['soft_deadline'] = $soft_deadline;
            }

            $retry_loop = $this->get_agent_loop(
                array(
                    'cancel_probe'                => $cancel_probe,
                    'progress_callback'           => $progress_callback,
                    'after_tool_call'             => $after_tool_call,
                    'follow_up_messages_provider' => $follow_up_messages_provider,
                    'max_iterations'              => $this->resolve_max_iterations( $mode, $retry_options ),
                )
            );
            $retry_result = $retry_loop->run( $bridge, $prompt, $retry_context, $retry_options );
            $retry_result = $this->attach_runtime_execution_metadata(
                $retry_result,
                $executed_tools,
                $successful_write_execution,
                $execution_contract
            );
            $attempt_records[] = array(
                'attempt' => $attempt_count,
                'status'  => ! empty( $retry_result['success'] ) ? 'completed' : ( ! empty( $retry_result['cancelled'] ) ? 'cancelled' : 'failed' ),
                'path'    => $this->describe_task_adequacy_recovery_path( $reflection ),
                'error'   => isset( $retry_result['error'] ) ? $this->truncate_execution_contract_text( (string) $retry_result['error'], 220 ) : '',
            );

            if ( ! empty( $retry_result['cancelled'] ) ) {
                $retry_result['task_adequacy_recovery_attempts'] = $attempt_records;
                return $this->merge_task_recovery_result( $result, $retry_result, $attempt_records );
            }

            if ( ! empty( $subagent_dispatch_successful ) ) {
                $retry_result = $this->mark_subagent_handoff_pending( $session_id, $prompt, $retry_result, $pending_subagent_jobs, $task_identity );
                $retry_result['task_adequacy_recovery_attempts'] = $attempt_records;
                return $this->merge_task_recovery_result( $result, $retry_result, $attempt_records );
            }

            $result = $this->merge_task_recovery_result( $result, $retry_result, $attempt_records );
            $result = $this->attach_runtime_execution_metadata(
                $result,
                $executed_tools,
                $successful_write_execution,
                $execution_contract
            );
            if ( $this->has_pending_extension_workflow_for_session( $session_id, $prompt ) ) {
                return $result;
            }

            $budget = $this->get_task_attempt_budget_for_prompt( $session_id, $prompt, $task_identity );
            if ( empty( $result['success'] ) ) {
                $result['success'] = true;
                $result['reply']   = 'Recovery attempt failed: ' . ( isset( $retry_result['error'] ) ? (string) $retry_result['error'] : 'unknown error' );
            }
        }

        return $this->block_task_after_two_attempts( $session_id, $prompt, $result, $budget, isset( $result['task_adequacy_reflection'] ) ? $result['task_adequacy_reflection'] : array(), $task_identity );
    }

    private function apply_task_checkpoint_result( $result, $checkpoint, $execution_contract ) {
        if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Task_Checkpoint' ) ) {
            return is_array( $result ) ? $result : array();
        }

        return Niwa_AI_Admin_Autonomy_Task_Checkpoint::apply_to_result(
            $result,
            $checkpoint,
            $this->describe_execution_contract_pending_requirements( $execution_contract, 8 )
        );
    }

    private function checkpoint_write_reconciliation( $context, $result, $successful_write_execution ) {
        $context = is_array( $context ) ? $context : array();
        $result  = is_array( $result ) ? $result : array();
        if (
            ! empty( $result['success'] )
            || ! empty( $result['cancelled'] )
            || empty( $successful_write_execution )
            || empty( $result['context_snapshot']['messages'] )
            || ! is_array( $result['context_snapshot']['messages'] )
            || ! class_exists( 'Niwa_AI_Admin_Autonomy_Task_Integrity' )
        ) {
            return $result;
        }

        $recovery_context = Niwa_AI_Admin_Autonomy_Task_Integrity::build_recovery_context(
            $context,
            $result,
            '[WRITE RECONCILIATION CONTINUATION] A write already succeeded before the model stream failed. Preserve the chronological trace and structured tool evidence. Do not repeat any successful write. Produce the final founder reply from the saved evidence, or inspect only what is still needed to verify that evidence.'
        );
        if ( empty( $recovery_context['continuation_context'] ) || ! is_array( $recovery_context['continuation_context'] ) ) {
            return $result;
        }

        $result['context_snapshot']      = $recovery_context['continuation_context'];
        $result['cancelled']             = true;
        $result['cancelled_reason']      = 'write_reconciliation';
        $result['task_completion_state'] = 'write_done_reply_pending';
        $result['reply']                 = '';
        return $result;
    }

    private function resolve_continuation_execution_provenance( $context ) {
        $continuation = isset( $context['continuation_context'] ) && is_array( $context['continuation_context'] )
            ? $context['continuation_context']
            : array();
        $provenance = isset( $continuation['execution_provenance'] ) && is_array( $continuation['execution_provenance'] )
            ? $continuation['execution_provenance']
            : array();
        $evidence = isset( $continuation['tool_evidence'] ) && is_array( $continuation['tool_evidence'] )
            ? $continuation['tool_evidence']
            : array();
        $executed_tools = isset( $provenance['executed_tools'] ) && is_array( $provenance['executed_tools'] )
            ? array_values( array_filter( array_map( 'sanitize_key', $provenance['executed_tools'] ) ) )
            : array();
        $successful_write_execution = ! empty( $provenance['successful_write_execution'] );

        foreach ( $evidence as $entry ) {
            if ( ! is_array( $entry ) || empty( $entry['succeeded'] ) ) {
                continue;
            }
            $tool_name = sanitize_key( (string) ( $entry['tool'] ?? '' ) );
            if ( '' !== $tool_name ) {
                $executed_tools[] = $tool_name;
            }
            if ( ! empty( $entry['write'] ) ) {
                $successful_write_execution = true;
            }
        }

        return array(
            'executed_tools'             => array_values( array_unique( $executed_tools ) ),
            'successful_write_execution' => $successful_write_execution,
        );
    }

    private function mark_subagent_handoff_pending( $session_id, $prompt, $result, $jobs, $task_identity = '' ) {
        $result = is_array( $result ) ? $result : array();
        $jobs   = is_array( $jobs ) ? array_values( $jobs ) : array();
        $this->update_task_attempt_budget_for_prompt(
            $session_id,
            $prompt,
            array(
                'status'          => 'pending_subagent',
                'attempt_count'   => 0,
                'max_attempts'    => self::MAX_TASK_RECOVERY_ATTEMPTS,
                'last_blocker'    => '',
                'attempted_paths' => array(),
            ),
            $task_identity
        );

        $result['success']                      = true;
        $result['subagent_dispatch_successful'] = true;
        $result['pending_subagent_jobs']        = $jobs;
        $result['task_completion_state']        = 'pending_subagent';
        $result['original_task_completed']      = false;
        $result['retry_original_task']          = false;
        $result['task_attempt_budget_status']   = 'pending_subagent';
        return $result;
    }

    private function should_run_task_adequacy_reflect( $context, $options, $mode, $result ) {
        $context = is_array( $context ) ? $context : array();
        $options = is_array( $options ) ? $options : array();
        $result  = is_array( $result ) ? $result : array();

        if (
            'main' !== sanitize_key( (string) $mode )
            || empty( $result['success'] )
            || ! empty( $result['cancelled'] )
            || ! empty( $result['task_checkpoint_state'] )
        ) {
            return false;
        }
        if ( ! empty( $result['subagent_dispatch_successful'] ) || 'pending_subagent' === sanitize_key( (string) ( $result['task_completion_state'] ?? '' ) ) ) {
            return false;
        }
        if (
            ! empty( $options['internal'] )
            || ! empty( $context['is_heartbeat'] )
            || ! empty( $context['is_operative_heartbeat'] )
            || ! empty( $context['is_proactive_task'] )
			|| ! empty( $context['is_daily_report'] )
            || ! empty( $context['scheduled_self_wake_execution'] )
            || ! empty( $context['scheduled_job'] )
			|| in_array( isset( $context['source'] ) ? sanitize_key( (string) $context['source'] ) : '', array( 'daily_report', 'scheduled_self_wake', 'scheduled_job' ), true )
        ) {
            return false;
        }
        if ( isset( $context['user_role'] ) && 'admin' !== (string) $context['user_role'] ) {
            return false;
        }
        if ( ! empty( $context['task_adequacy_reflection_disabled'] ) || ! empty( $options['task_adequacy_reflection_disabled'] ) ) {
            return false;
        }

        return true;
    }

    /**
     * Decide whether verifier failure must withhold the candidate reply.
     *
     * Runtime evidence, writes, media, continuations, and contracts fail closed.
     * A no-evidence conversational candidate fails open during verifier outage.
     * This deliberately avoids language-specific action words.
     */
    private function turn_has_integrity_sensitive_structure( $context, $options, $result ) {
        if (
            ! empty( $context['task_integrity_verification_required'] )
            || ! empty( $options['task_integrity_verification_required'] )
            || ! empty( $context['force_action'] )
            || ! empty( $options['force_action'] )
            || ! empty( $context['continuation_context'] )
            || ! empty( $options['task_adequacy_retry_attempt'] )
            || ! empty( $result['successful_write_execution'] )
            || ! empty( $result['executed_tools'] )
            || ! empty( $result['tool_evidence'] )
            || ! empty( $result['context_snapshot']['tool_evidence'] )
        ) {
            return true;
        }

        $contract = isset( $result['execution_contract'] ) && is_array( $result['execution_contract'] )
            ? $result['execution_contract']
            : array();
        if ( ! empty( $contract['pending'] ) || ! empty( $contract['completed'] ) ) {
            return true;
        }

        foreach ( array(
            'attachment_image_url',
            'attachment_image_urls',
            'attachment_image_attachment_id',
            'attachment_image_attachment_ids',
            'incoming_media',
            'image_urls',
            'images',
            'attachments',
            'media',
            'photo_file_ids',
            'document_file_ids',
        ) as $key ) {
            if ( ! empty( $context[ $key ] ) ) {
                return true;
            }
        }

        return false;
    }

    private function task_adequacy_reflect( $bridge, $prompt, $context, $options, $result, $budget ) {
        $fallback = array(
            'status'       => 'partial',
            'reason'       => 'reflection_unavailable',
            'blocker'      => 'task_integrity_verifier_unavailable',
            'safe_user_message' => '',
            'verification_unavailable' => true,
        );
        if ( ! is_object( $bridge ) || ! method_exists( $bridge, 'build_turn_runtime' ) ) {
            return $fallback;
        }

        $runtime = $bridge->build_turn_runtime(
            $prompt,
            array_merge( is_array( $context ) ? $context : array(), array( 'task_adequacy_reflect' => true ) ),
            'main',
            array_merge( is_array( $options ) ? $options : array(), array( 'tool_choice' => 'none' ) )
        );
        if ( empty( $runtime['success'] ) ) {
            return $fallback;
        }

        $model     = isset( $runtime['model'] ) ? (string) $runtime['model'] : 'gpt-5.4-mini';
        $auth_mode = isset( $runtime['auth_mode'] ) ? (string) $runtime['auth_mode'] : 'api_key';
        $creds     = isset( $runtime['creds'] ) && is_array( $runtime['creds'] ) ? $runtime['creds'] : array();
        $evaluation_context = class_exists( 'Niwa_AI_Admin_Autonomy_Task_Integrity' )
            ? Niwa_AI_Admin_Autonomy_Task_Integrity::build_evaluation_context( $context, $prompt )
            : array();
        $context_snapshot = isset( $result['context_snapshot'] ) && is_array( $result['context_snapshot'] ) ? $result['context_snapshot'] : array();
        $payload   = array(
            'original_task'  => $this->truncate_execution_contract_text( (string) $prompt, 2400 ),
            'candidate_reply' => $this->truncate_execution_contract_text( isset( $result['reply'] ) ? (string) $result['reply'] : '', 2400 ),
            'executed_tools' => isset( $result['executed_tools'] ) && is_array( $result['executed_tools'] ) ? array_values( $result['executed_tools'] ) : array(),
            'successful_write_execution' => ! empty( $result['successful_write_execution'] ),
            'execution_contract' => isset( $result['execution_contract'] ) && is_array( $result['execution_contract'] ) ? $result['execution_contract'] : array(),
            'tool_evidence' => isset( $context_snapshot['tool_evidence'] ) && is_array( $context_snapshot['tool_evidence'] ) ? $context_snapshot['tool_evidence'] : array(),
            'turn_frame' => isset( $context_snapshot['turn_frame'] ) && is_array( $context_snapshot['turn_frame'] ) ? $context_snapshot['turn_frame'] : array(),
            'conversation_context' => $evaluation_context,
            'attempt_budget' => is_array( $budget ) ? $budget : array(),
        );
        $messages = array(
            array(
                'role'    => 'system',
                'content' => 'task_adequacy_reflect: You are the final integrity gate for a multilingual founder agent. Judge the candidate against the chronological conversation, actual tool evidence, and execution contract, not against the current message in isolation. Session memory is advisory only and can never override those primary records. Return only one compact JSON object. Allowed status values: adequate, partial, needs_more_research, skill_gap, tool_gap, unsafe_or_disallowed, not_feasible. A final reply that contradicts completed tool work, asks the founder to restate context already present, claims image/catalog analysis without matching evidence, reports a partial batch as complete, or claims completion without sufficient evidence is partial. Informational or social replies do not require tool calls when they directly and correctly answer the conversation. Mark adequate only when the reply completes the task with sufficient evidence or is an explicitly acceptable answer. Mark skill_gap when reusable procedural knowledge may solve it. Mark tool_gap when an executable capability is missing. Dangerous, illegal, destructive, or clearly impossible tasks must be unsafe_or_disallowed or not_feasible and must not request recovery attempts. safe_user_message must be a short plain sentence in the same language as the founder conversation, written as the assistant speaking to the founder about what it still needs; it must never mention judging, candidates, evaluation, or this reflection step.',
            ),
            array(
                'role'    => 'user',
                'content' => wp_json_encode( $payload ),
            ),
        );

        $response = $this->openai_client->chat_complete( $model, $messages, $auth_mode, $creds, 25, null, 'none' );
        if ( empty( $response['success'] ) ) {
            return $fallback;
        }

        $answer = isset( $response['answer'] ) ? (string) $response['answer'] : '';
        $json   = $this->extract_json_object_from_text( $answer );
        if ( empty( $json ) ) {
            return $fallback;
        }

        $status = $this->normalize_task_adequacy_status( isset( $json['status'] ) ? (string) $json['status'] : '' );
        $json['status'] = '' !== $status ? $status : 'adequate';
        $json['reason'] = isset( $json['reason'] ) ? $this->truncate_execution_contract_text( (string) $json['reason'], 700 ) : '';
        $json['blocker'] = isset( $json['blocker'] ) ? $this->truncate_execution_contract_text( (string) $json['blocker'], 700 ) : '';
        $json['recommended_next_path'] = isset( $json['recommended_next_path'] ) ? $this->truncate_execution_contract_text( (string) $json['recommended_next_path'], 300 ) : '';
        $json['safe_user_message'] = isset( $json['safe_user_message'] ) ? $this->truncate_execution_contract_text( (string) $json['safe_user_message'], 700 ) : '';
        $json['reflection_tool'] = 'task_adequacy_reflect';
        $json['verification_unavailable'] = false;

        return $json;
    }

    private function normalize_task_adequacy_status( $status ) {
        $status = sanitize_key( (string) $status );
        return in_array( $status, array( 'adequate', 'partial', 'needs_more_research', 'skill_gap', 'tool_gap', 'unsafe_or_disallowed', 'not_feasible' ), true )
            ? $status
            : 'partial';
    }

    private function build_agentic_growth_retry_context( $prompt, $result, $reflection, $budget ) {
        $lines = array(
            '[AGENTIC GROWTH RETRY]',
            'The previous answer was judged inadequate by task_adequacy_reflect. Continue the original task now instead of apologizing or ending.',
            'Original task: ' . $this->truncate_execution_contract_text( (string) $prompt, 900 ),
            'Adequacy status: ' . ( isset( $reflection['status'] ) ? (string) $reflection['status'] : 'partial' ),
            'Reason: ' . ( isset( $reflection['reason'] ) ? $this->truncate_execution_contract_text( (string) $reflection['reason'], 700 ) : '' ),
            'Recommended next path: ' . ( isset( $reflection['recommended_next_path'] ) ? $this->truncate_execution_contract_text( (string) $reflection['recommended_next_path'], 300 ) : '' ),
            'Attempts used: ' . intval( isset( $budget['attempt_count'] ) ? $budget['attempt_count'] : 0 ) . '/' . self::MAX_TASK_RECOVERY_ATTEMPTS,
            'Allowed recovery paths: use more suitable existing tools, tool_search, skill_search, external_skill_search, external_skill_get, approved skill_import_from_github, extension_request_create, a focused subagent, or retry the task with better evidence.',
            'If the task is unsafe, illegal, destructive, or impossible in this environment, stop and say that clearly with a safe alternative.',
            'If a remote skill or runtime import is needed, do not import without founder approval. Open extension_request_create with the evidence and selected skill/package.',
            'After a successful retry, save the reusable path with skill_create or skill_update when the success teaches a durable workflow.',
        );
        if ( ! empty( $result['reply'] ) ) {
            $lines[] = 'Previous reply: ' . $this->truncate_execution_contract_text( (string) $result['reply'], 900 );
        }
        if ( ! empty( $budget['attempted_paths'] ) && is_array( $budget['attempted_paths'] ) ) {
            $lines[] = 'Already attempted paths: ' . implode( ', ', array_slice( array_map( 'strval', $budget['attempted_paths'] ), 0, 8 ) );
        }

        return implode( "\n", $lines ) . "\n[/AGENTIC GROWTH RETRY]";
    }

    private function describe_task_adequacy_recovery_path( $reflection ) {
        $reflection = is_array( $reflection ) ? $reflection : array();
        $status = isset( $reflection['status'] ) ? sanitize_key( (string) $reflection['status'] ) : 'partial';
        $path   = isset( $reflection['recommended_next_path'] ) ? trim( (string) $reflection['recommended_next_path'] ) : '';
        if ( '' === $path ) {
            $path = $status;
        }

        return $this->truncate_execution_contract_text( $path, 160 );
    }

    private function resolve_task_attempt_identity( $prompt, $context, $options, $result ) {
        $context = is_array( $context ) ? $context : array();
        $options = is_array( $options ) ? $options : array();
        $result  = is_array( $result ) ? $result : array();
        $continuation = isset( $context['continuation_context'] ) && is_array( $context['continuation_context'] )
            ? $context['continuation_context']
            : array();
        $snapshot = isset( $result['context_snapshot'] ) && is_array( $result['context_snapshot'] )
            ? $result['context_snapshot']
            : array();

        foreach ( array(
            $continuation['task_integrity_id'] ?? '',
            $snapshot['task_integrity_id'] ?? '',
            $options['task_integrity_id'] ?? '',
        ) as $candidate ) {
            $candidate = trim( (string) $candidate );
            if ( '' !== $candidate ) {
                return $this->truncate_execution_contract_text( $candidate, 180 );
            }
        }

        $turn_id = trim( (string) ( $options['turn_id'] ?? ( $context['turn_id'] ?? '' ) ) );
        if ( '' !== $turn_id ) {
            return 'turn:' . $this->truncate_execution_contract_text( $turn_id, 160 );
        }

        $history = isset( $context['history'] ) && is_array( $context['history'] )
            ? array_slice( array_values( $context['history'] ), -2 )
            : array();
        $seed = wp_json_encode(
            array(
                'prompt'  => trim( (string) $prompt ),
                'history' => $history,
            ),
            JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
        );

        return 'task:' . hash( 'sha256', is_string( $seed ) ? $seed : trim( (string) $prompt ) );
    }

    private function get_task_attempt_budget_for_prompt( $session_id, $prompt, $task_identity = '' ) {
        if ( '' !== trim( (string) $session_id ) && $this->session_state && method_exists( $this->session_state, 'get_task_attempt_budget_state' ) ) {
            $state = (array) $this->session_state->get_task_attempt_budget_state( $session_id, $prompt, $task_identity );
            if ( ! empty( $state ) ) {
                return $state;
            }
        }

        return array(
            'original_task_hash' => hash( 'sha256', trim( preg_replace( '/\s+/', ' ', (string) $prompt ) ) ),
            'task_identity_hash' => hash( 'sha256', '' !== trim( (string) $task_identity ) ? (string) $task_identity : trim( preg_replace( '/\s+/', ' ', (string) $prompt ) ) ),
            'original_task'      => (string) $prompt,
            'attempt_count'      => 0,
            'max_attempts'       => self::MAX_TASK_RECOVERY_ATTEMPTS,
            'last_blocker'       => '',
            'attempted_paths'    => array(),
            'status'             => 'idle',
        );
    }

    private function update_task_attempt_budget_for_prompt( $session_id, $prompt, $updates, $task_identity = '' ) {
        $updates = is_array( $updates ) ? $updates : array();
        if ( '' !== trim( (string) $session_id ) && $this->session_state && method_exists( $this->session_state, 'update_task_attempt_budget_state' ) ) {
            $state = (array) $this->session_state->update_task_attempt_budget_state( $session_id, $prompt, $updates, $task_identity );
            if ( ! empty( $state ) ) {
                return $state;
            }
        }

        return array_merge( $this->get_task_attempt_budget_for_prompt( $session_id, $prompt, $task_identity ), $updates );
    }

    private function block_task_after_two_attempts( $session_id, $prompt, $result, $budget, $reflection, $task_identity = '' ) {
        $result     = is_array( $result ) ? $result : array();
        $budget     = is_array( $budget ) ? $budget : array();
        $reflection = is_array( $reflection ) ? $reflection : array();
        $attempted  = isset( $budget['attempted_paths'] ) && is_array( $budget['attempted_paths'] ) ? array_values( $budget['attempted_paths'] ) : array();
        $blocker    = isset( $reflection['blocker'] ) && '' !== trim( (string) $reflection['blocker'] )
            ? trim( (string) $reflection['blocker'] )
            : ( isset( $reflection['reason'] ) ? trim( (string) $reflection['reason'] ) : '' );
        if ( '' === $blocker && ! empty( $budget['last_blocker'] ) ) {
            $blocker = (string) $budget['last_blocker'];
        }

        $this->update_task_attempt_budget_for_prompt(
            $session_id,
            $prompt,
            array(
                'status'          => 'blocked_after_two_attempts',
                'attempt_count'   => self::MAX_TASK_RECOVERY_ATTEMPTS,
                'max_attempts'    => self::MAX_TASK_RECOVERY_ATTEMPTS,
                'last_blocker'    => $blocker,
                'attempted_paths' => $attempted,
            ),
            $task_identity
        );

        // The raw judge blocker/reason and attempted-path keys are internal
        // reflection output (often English grader phrasing) — keep them in the
        // attempt budget state and logs, never in the user-facing reply.
        $safe = isset( $reflection['safe_user_message'] ) ? trim( (string) $reflection['safe_user_message'] ) : '';
        if ( '' !== $safe ) {
			$reply = $safe;
        } else {
			$reply = __( 'I could not safely finish this task with the available evidence. Please provide the missing input or approval so I can continue from the saved work.', 'niwa-ai' );
        }

        $result['success'] = true;
		$result['reply']   = $reply;
        $result['task_attempt_budget_status'] = 'blocked_after_two_attempts';
        $result['task_attempt_block_blocker'] = $this->truncate_execution_contract_text( $blocker, 420 );
        $result['task_attempt_block_paths']   = array_slice( array_map( 'strval', $attempted ), 0, 4 );

        return $result;
    }

    private function result_is_extension_approval_ask( $result, $session_id ) {
        $result = is_array( $result ) ? $result : array();
        if ( '' === trim( isset( $result['reply'] ) ? (string) $result['reply'] : '' ) ) {
            return false;
        }

        $executed_tools = isset( $result['executed_tools'] ) && is_array( $result['executed_tools'] ) ? $result['executed_tools'] : array();
        if ( ! in_array( 'extension_request_create', $executed_tools, true ) ) {
            return false;
        }

        return $this->has_pending_extension_workflow_for_session( $session_id );
    }

    private function has_pending_extension_workflow_for_session( $session_id, $prompt = '' ) {
        if ( '' === trim( (string) $session_id ) || ! $this->session_state || ! method_exists( $this->session_state, 'get_extension_workflow_state' ) ) {
            return false;
        }

        $workflow = (array) $this->session_state->get_extension_workflow_state( $session_id, $prompt );
        $status   = isset( $workflow['status'] ) ? sanitize_key( (string) $workflow['status'] ) : '';
        return 'awaiting_approval' === $status;
    }

    private function merge_task_recovery_result( $previous, $retry, $attempt_records ) {
        $previous = is_array( $previous ) ? $previous : array();
        $retry    = is_array( $retry ) ? $retry : array();
        $merged   = array_merge( $previous, $retry );
        $merged['task_adequacy_recovery_attempts'] = is_array( $attempt_records ) ? array_values( $attempt_records ) : array();
        $merged['previous_reply_before_recovery'] = isset( $previous['reply'] ) ? (string) $previous['reply'] : '';

        $merged['iterations'] = intval( isset( $previous['iterations'] ) ? $previous['iterations'] : 0 ) + intval( isset( $retry['iterations'] ) ? $retry['iterations'] : 0 );
        $merged['usage']      = $this->merge_usage_arrays(
            isset( $previous['usage'] ) && is_array( $previous['usage'] ) ? $previous['usage'] : array(),
            isset( $retry['usage'] ) && is_array( $retry['usage'] ) ? $retry['usage'] : array()
        );

        return $merged;
    }

    private function attach_runtime_execution_metadata( $result, $executed_tools, $successful_write_execution, $execution_contract ) {
        $result             = is_array( $result ) ? $result : array();
        $existing_tools     = isset( $result['executed_tools'] ) && is_array( $result['executed_tools'] ) ? $result['executed_tools'] : array();
        $runtime_tools      = is_array( $executed_tools ) ? $executed_tools : array();
        $result['executed_tools'] = array_values(
            array_unique(
                array_filter(
                    array_map( 'sanitize_key', array_merge( $existing_tools, $runtime_tools ) )
                )
            )
        );
        $result['successful_write_execution'] = ! empty( $result['successful_write_execution'] ) || ! empty( $successful_write_execution );
        $result['execution_contract'] = $this->summarize_execution_contract(
            is_array( $execution_contract ) ? $execution_contract : array()
        );

        return $result;
    }

    private function merge_usage_arrays( $left, $right ) {
        $keys = array( 'prompt_tokens', 'completion_tokens', 'total_tokens', 'cached_tokens' );
        $out  = array();
        foreach ( $keys as $key ) {
            $out[ $key ] = intval( isset( $left[ $key ] ) ? $left[ $key ] : 0 ) + intval( isset( $right[ $key ] ) ? $right[ $key ] : 0 );
        }
        $prompt = max( 0, intval( isset( $out['prompt_tokens'] ) ? $out['prompt_tokens'] : 0 ) );
        $cached = max( 0, intval( isset( $out['cached_tokens'] ) ? $out['cached_tokens'] : 0 ) );
        $out['cache_hit_rate'] = $prompt > 0 ? round( min( $cached, $prompt ) / $prompt, 4 ) : 0.0;

        return $out;
    }

    private function extract_json_object_from_text( $text ) {
        $text = trim( (string) $text );
        if ( '' === $text ) {
            return array();
        }
        $text = preg_replace( '/^```(?:json)?\s*/i', '', $text );
        $text = preg_replace( '/\s*```$/', '', $text );
        $decoded = json_decode( $text, true );
        if ( is_array( $decoded ) ) {
            return $decoded;
        }

        $start = strpos( $text, '{' );
        $end   = strrpos( $text, '}' );
        if ( false === $start || false === $end || $end <= $start ) {
            return array();
        }
        $decoded = json_decode( substr( $text, $start, $end - $start + 1 ), true );

        return is_array( $decoded ) ? $decoded : array();
    }

    /**
     * Get or create the Agent Loop instance.
     *
     * @param array $runtime_options cancel_probe, progress_callback etc.
     * @return Niwa_AI_Agent_Loop
     */
	private function get_agent_loop( $runtime_options = array() ) {
        // Create a fresh loop each time so runtime options (cancel_probe, etc.) are current.
		return new Niwa_AI_Agent_Loop( $this->openai_client, array(
			'cancel_probe'                => isset( $runtime_options['cancel_probe'] ) ? $runtime_options['cancel_probe'] : null,
            'progress_callback'           => isset( $runtime_options['progress_callback'] ) ? $runtime_options['progress_callback'] : null,
            'after_tool_call'             => isset( $runtime_options['after_tool_call'] ) && is_callable( $runtime_options['after_tool_call'] ) ? $runtime_options['after_tool_call'] : null,
            'max_iterations'              => isset( $runtime_options['max_iterations'] ) ? intval( $runtime_options['max_iterations'] ) : Niwa_AI_Agent_Loop::MAX_ITERATIONS,
            'session_state'               => $this->session_state,
            'steering_messages_provider'  => isset( $runtime_options['steering_messages_provider'] ) && is_callable( $runtime_options['steering_messages_provider'] ) ? $runtime_options['steering_messages_provider'] : $this->steering_messages_provider,
            'follow_up_messages_provider' => isset( $runtime_options['follow_up_messages_provider'] ) && is_callable( $runtime_options['follow_up_messages_provider'] ) ? $runtime_options['follow_up_messages_provider'] : $this->follow_up_messages_provider,
		) );
	}

	/**
	 * Resolve the per-turn agent iteration cap.
	 *
	 * @param string $mode    Turn mode.
	 * @param array  $options Turn options.
	 * @return int
	 */
	private function resolve_max_iterations( $mode, $options ) {
		if ( isset( $options['max_iterations'] ) ) {
			return max( 1, intval( $options['max_iterations'] ) );
		}

		return Niwa_AI_Agent_Loop::MAX_ITERATIONS;
	}

	/**
	 * Persist a compact prompt-cache usage snapshot for the runtime monitor.
	 *
	 * @param array  $result  Agent loop result.
	 * @param array  $context Turn context.
	 * @param string $channel Runtime channel.
	 * @param string $mode    Runtime mode.
	 * @return void
	 */
	private function record_prompt_cache_usage_snapshot( $result, $context, $channel, $mode ) {
		if ( ! is_array( $result ) || empty( $result['usage'] ) || ! is_array( $result['usage'] ) ) {
			return;
		}

		$usage      = $result['usage'];
		$prompt     = isset( $usage['prompt_tokens'] ) ? max( 0, intval( $usage['prompt_tokens'] ) ) : 0;
		$cached     = isset( $usage['cached_tokens'] ) ? max( 0, intval( $usage['cached_tokens'] ) ) : 0;
		$completion = isset( $usage['completion_tokens'] ) ? max( 0, intval( $usage['completion_tokens'] ) ) : 0;
		$total      = isset( $usage['total_tokens'] ) ? max( 0, intval( $usage['total_tokens'] ) ) : ( $prompt + $completion );

		if ( $prompt <= 0 && $total <= 0 ) {
			return;
		}

		$cached = min( $cached, $prompt );
		$rate   = $prompt > 0 ? round( $cached / $prompt, 4 ) : 0.0;
		if ( isset( $usage['cache_hit_rate'] ) && is_numeric( $usage['cache_hit_rate'] ) ) {
			$rate = max( 0.0, min( 1.0, round( (float) $usage['cache_hit_rate'], 4 ) ) );
		}

		$previous = get_option( Niwa_AI_Constants::OPT_AUTONOMY_PROMPT_CACHE_USAGE, array() );
		$previous = is_array( $previous ) ? $previous : array();

		$aggregate_prompt = isset( $previous['aggregate_prompt_tokens'] ) ? max( 0, intval( $previous['aggregate_prompt_tokens'] ) ) : 0;
		$aggregate_cached = isset( $previous['aggregate_cached_tokens'] ) ? max( 0, intval( $previous['aggregate_cached_tokens'] ) ) : 0;
		$samples          = isset( $previous['sample_count'] ) ? max( 0, intval( $previous['sample_count'] ) ) : 0;

		$aggregate_prompt += $prompt;
		$aggregate_cached += $cached;
		$samples++;

		$snapshot = array(
			'captured_at'       => time(),
			'model'             => isset( $result['model'] ) ? sanitize_text_field( (string) $result['model'] ) : '',
			'channel'           => sanitize_key( (string) $channel ),
			'mode'              => sanitize_key( (string) $mode ),
			'session_id'        => isset( $context['session_id'] ) ? sanitize_text_field( (string) $context['session_id'] ) : '',
			'turn_id'           => isset( $result['turn_id'] ) ? sanitize_text_field( (string) $result['turn_id'] ) : '',
			'prompt_tokens'     => $prompt,
			'cached_tokens'     => $cached,
			'completion_tokens' => $completion,
			'total_tokens'      => $total,
			'cache_hit_rate'    => $rate,
		);

		$recent = isset( $previous['recent'] ) && is_array( $previous['recent'] ) ? $previous['recent'] : array();
		array_unshift( $recent, $snapshot );
		$recent = array_slice( $recent, 0, 12 );

		update_option(
			Niwa_AI_Constants::OPT_AUTONOMY_PROMPT_CACHE_USAGE,
			array(
				'last'                        => $snapshot,
				'recent'                      => $recent,
				'sample_count'                => $samples,
				'aggregate_prompt_tokens'     => $aggregate_prompt,
				'aggregate_cached_tokens'     => $aggregate_cached,
				'aggregate_cache_hit_rate'    => $aggregate_prompt > 0 ? round( min( $aggregate_cached, $aggregate_prompt ) / $aggregate_prompt, 4 ) : 0.0,
				'updated_at'                  => time(),
			),
			false
		);
	}

    private function finish_turn( $session_id, $turn, $reply, $status, $meta = array() ) {
        if ( ! $this->session_state ) {
            return;
        }

        $this->session_state->finish_turn(
            $session_id,
            isset( $turn['id'] ) ? (string) $turn['id'] : '',
            $reply,
            $status,
            $meta
        );
    }

    private function apply_internal_finish_meta( $finish_meta, $meta_options ) {
        $finish_meta  = is_array( $finish_meta ) ? $finish_meta : array();
        $meta_options = is_array( $meta_options ) ? $meta_options : array();

        if ( ! empty( $meta_options['internal'] ) ) {
            $finish_meta['internal']   = true;
            $finish_meta['visibility'] = 'internal';
        }

        return $finish_meta;
    }

    private function should_cancel_turn( $cancel_probe ) {
        if ( ! is_callable( $cancel_probe ) ) {
            return false;
        }

        return ! empty( call_user_func( $cancel_probe ) );
    }

    private function decode_tool_payload_content( $tool_payload ) {
        if ( ! is_array( $tool_payload ) || empty( $tool_payload['content'] ) ) {
            return array();
        }

        $decoded = json_decode( (string) $tool_payload['content'], true );
        return is_array( $decoded ) ? $decoded : array();
    }

    private function did_tool_payload_succeed( $decoded_payload ) {
        if ( ! is_array( $decoded_payload ) ) {
            return false;
        }

        if ( array_key_exists( 'ok', $decoded_payload ) ) {
            return ! empty( $decoded_payload['ok'] );
        }

        if ( array_key_exists( 'success', $decoded_payload ) ) {
            return ! empty( $decoded_payload['success'] );
        }

        return empty( $decoded_payload['error'] );
    }

    private function initialize_execution_contract() {
        return array(
            'pending'                 => array(),
            'completed'               => array(),
            'events'                  => array(),
            'search_candidates'       => array(),
            'runtime_reload_required' => false,
        );
    }

    private function should_enforce_execution_contract_for_turn( $context, $options ) {
        $context = is_array( $context ) ? $context : array();
        $options = is_array( $options ) ? $options : array();

        if ( ! empty( $context['force_action'] ) || ! empty( $options['force_action'] ) ) {
            return true;
        }

        if ( ! empty( $context['is_heartbeat'] ) || ! empty( $context['is_operative_heartbeat'] ) || ! empty( $context['is_proactive_task'] ) ) {
            return true;
        }

        if ( ! empty( $options['internal'] ) && ( ! empty( $context['heartbeat_task'] ) || ! empty( $context['continuation_context'] ) ) ) {
            return true;
        }

        return false;
    }

    private function should_require_task_checkpoint_for_turn( $context, $mode, $options ) {
        $context = is_array( $context ) ? $context : array();
        $options = is_array( $options ) ? $options : array();
        if (
            'main' !== sanitize_key( (string) $mode )
            || 'admin' !== sanitize_key( (string) ( $context['user_role'] ?? 'admin' ) )
            || ! empty( $context['task_checkpoint_disabled'] )
            || ! empty( $options['task_checkpoint_disabled'] )
        ) {
            return false;
        }

        $source = sanitize_key( (string) ( $context['source'] ?? '' ) );
        if (
            ! empty( $context['is_heartbeat'] )
            || ! empty( $context['is_operative_heartbeat'] )
            || ! empty( $context['is_proactive_task'] )
            || ! empty( $context['scheduled_self_wake_execution'] )
            || ! empty( $context['scheduled_job'] )
            || in_array( $source, array( 'scheduled_self_wake', 'scheduled_job', 'heartbeat', 'proactive_task', 'autonomous_task' ), true )
        ) {
            return false;
        }

        return empty( $options['internal'] ) || ! empty( $context['continuation_context'] );
    }

    private function update_execution_contract_from_tool_call( $contract, $tool_name, $decoded_payload, $tool_succeeded, $enforce_tool_chain_contract = false, $bridge = null, $require_task_checkpoint = false ) {
        $contract        = $this->normalize_execution_contract( $contract );
        $tool_name       = sanitize_key( (string) $tool_name );
        $decoded_payload = is_array( $decoded_payload ) ? $decoded_payload : array();
        $loaded_names    = array();
        $loaded_write_tools = array();

        if ( '' === $tool_name ) {
            return $contract;
        }

        $contract['events'][] = array(
            'tool'    => $tool_name,
            'success' => ! empty( $tool_succeeded ),
            'time'    => time(),
            'error'   => ! empty( $decoded_payload['error'] ) ? $this->truncate_execution_contract_text( (string) $decoded_payload['error'], 180 ) : '',
        );
        if ( count( $contract['events'] ) > 20 ) {
            $contract['events'] = array_slice( $contract['events'], -20 );
        }

        if ( 'task_checkpoint' === $tool_name ) {
            if ( ! empty( $tool_succeeded ) ) {
                $state    = sanitize_key( (string) ( $decoded_payload['task_checkpoint_state'] ?? '' ) );
                $contract = $this->complete_execution_contract_requirement(
                    $contract,
                    'task_checkpoint',
                    $tool_name,
                    'Structured task lifecycle checkpoint recorded as ' . $state . '.'
                );
                if ( 'blocked' === $state ) {
                    $contract = $this->complete_all_execution_contract_requirements(
                        $contract,
                        $tool_name,
                        'Structured task checkpoint recorded a concrete blocker.'
                    );
                }
            }
            return $contract;
        }

        if ( 'tool_load' === $tool_name && ! empty( $tool_succeeded ) && ! empty( $decoded_payload['loaded'] ) && is_array( $decoded_payload['loaded'] ) ) {
            $loaded_names = array_values(
                array_filter(
                    array_map(
                        function ( $name ) {
                            return sanitize_key( (string) $name );
                        },
                        $decoded_payload['loaded']
                    )
                )
            );
            $loaded_write_tools = array_values(
                array_filter(
                    $loaded_names,
                    function ( $name ) use ( $bridge ) {
                        return $this->is_write_tool_name_for_guard( $name, $bridge );
                    }
                )
            );
        }

        if ( empty( $enforce_tool_chain_contract ) && empty( $loaded_write_tools ) && empty( $require_task_checkpoint ) ) {
            return $contract;
        }

        if ( 'tool_search' === $tool_name && ! empty( $tool_succeeded ) && ! empty( $decoded_payload['capability_gap'] ) && ! empty( $decoded_payload['extension_allowed'] ) ) {
            $contract = $this->add_execution_contract_requirement(
                $contract,
                'extension_request_after_gap_search',
                'Open a capability-gap self-extension request before claiming the task is impossible.',
                $tool_name,
                array(
                    'kind'               => 'capability_gap_request',
                    'missing_capability' => ! empty( $decoded_payload['missing_capability'] ) ? sanitize_key( (string) $decoded_payload['missing_capability'] ) : 'runtime_capability_gap',
                    'search_query'       => isset( $decoded_payload['search_query'] ) ? (string) $decoded_payload['search_query'] : '',
                    'candidate_tools'    => isset( $decoded_payload['candidate_tools'] ) && is_array( $decoded_payload['candidate_tools'] ) ? $decoded_payload['candidate_tools'] : array(),
                )
            );
        } elseif ( 'tool_search' === $tool_name && ! empty( $tool_succeeded ) && ! empty( $decoded_payload['results'] ) && is_array( $decoded_payload['results'] ) ) {
            $contract['search_candidates'] = array_values(
                array_unique(
                    array_filter(
                        array_map(
                            static function ( $row ) {
                                return is_array( $row ) && ! empty( $row['name'] )
                                    ? sanitize_key( (string) $row['name'] )
                                    : '';
                            },
                            array_slice( $decoded_payload['results'], 0, 8 )
                        )
                    )
                )
            );
        }

        if ( 'extension_request_create' === $tool_name && ! empty( $tool_succeeded ) ) {
            $contract = $this->complete_execution_contract_requirements_by_kind( $contract, 'capability_gap_request', $tool_name, 'Capability-gap request was opened for founder approval.' );
        }

        if ( ! empty( $tool_succeeded ) && $this->is_capability_gap_resolution_tool( $tool_name ) ) {
            $contract = $this->complete_execution_contract_requirements_by_kind(
                $contract,
                'capability_gap_request',
                $tool_name,
                'A concrete runtime tool executed successfully, so no reusable extension is required.'
            );
        }

        if ( 'tool_load' === $tool_name && ! empty( $tool_succeeded ) && ! empty( $loaded_write_tools ) ) {
            $loaded_from_search = array_intersect(
                $loaded_write_tools,
                isset( $contract['search_candidates'] ) && is_array( $contract['search_candidates'] )
                    ? $contract['search_candidates']
                    : array()
            );
            if ( ! empty( $loaded_from_search ) ) {
                $contract = $this->add_execution_contract_requirement(
                    $contract,
                    'execute_loaded_tools',
                    'Use the loaded write tool in this same turn before giving the final answer.',
                    $tool_name,
                    array(
                        'kind'   => 'tool_execution',
                        'loaded' => array_values( $loaded_from_search ),
                    )
                );
            }
            $contract['search_candidates'] = array();
        }

        if ( ! empty( $tool_succeeded ) && ! $this->is_tool_management_name( $tool_name ) ) {
            $contract = $this->complete_loaded_tool_execution_requirement( $contract, $tool_name, $bridge );
            if ( $this->is_write_tool_name_for_guard( $tool_name, $bridge ) ) {
                $contract = $this->complete_execution_contract_requirements_by_kind( $contract, 'retry_execution', $tool_name, 'A concrete execution tool succeeded after the failed attempt.' );
            }
            $contract = $this->complete_execution_contract_fallbacks_for_tool( $contract, $tool_name );
        }

        if ( ! empty( $decoded_payload['fallback_tool'] ) ) {
            $fallback_tool = sanitize_key( (string) $decoded_payload['fallback_tool'] );
            if ( '' !== $fallback_tool && $fallback_tool !== $tool_name ) {
                $contract = $this->add_execution_contract_requirement(
                    $contract,
                    'fallback_' . $fallback_tool,
                    'Call fallback tool "' . $fallback_tool . '" in this same turn.',
                    $tool_name,
                    array(
                        'kind' => 'fallback',
                        'tool' => $fallback_tool,
                    )
                );
            }
        } elseif ( empty( $tool_succeeded ) && $this->is_write_tool_name_for_guard( $tool_name, $bridge ) ) {
            $contract = $this->add_execution_contract_requirement(
                $contract,
                'retry_execution_' . $tool_name,
                'The execution tool "' . $tool_name . '" failed. Retry with corrected arguments or report a real blocker from tool output.',
                $tool_name,
                array(
                    'kind' => 'retry_execution',
                    'tool' => $tool_name,
                )
            );
        }

        if ( ! empty( $tool_succeeded ) && ! empty( $decoded_payload['runtime_reload_required'] ) ) {
            $contract['runtime_reload_required'] = true;
            $contract = $this->complete_execution_contract_requirements_by_kind( $contract, 'retry_execution', $tool_name, 'Runtime code changed; continuation requires the next request.' );
        }

        $contract = $this->update_execution_contract_from_postconditions( $contract, $tool_name, $decoded_payload );
        if (
            ! empty( $require_task_checkpoint )
            && ! $this->is_tool_management_name( $tool_name )
            && ! $this->tool_payload_owns_terminal_lifecycle( $tool_name, $decoded_payload, $tool_succeeded )
        ) {
            $contract = $this->add_execution_contract_requirement(
                $contract,
                'task_checkpoint',
                'Call task_checkpoint as the final tool: completed only when the whole original task is verified, continue when work remains, or blocked for a concrete external blocker.',
                $tool_name,
                array(
                    'kind' => 'task_checkpoint',
                )
            );
        }

        return $contract;
    }

    private function normalize_execution_contract( $contract ) {
        $contract = is_array( $contract ) ? $contract : array();

        return array(
            'pending'                 => isset( $contract['pending'] ) && is_array( $contract['pending'] ) ? $contract['pending'] : array(),
            'completed'               => isset( $contract['completed'] ) && is_array( $contract['completed'] ) ? $contract['completed'] : array(),
            'events'                  => isset( $contract['events'] ) && is_array( $contract['events'] ) ? $contract['events'] : array(),
            'search_candidates'       => isset( $contract['search_candidates'] ) && is_array( $contract['search_candidates'] )
                ? array_values( array_unique( array_filter( array_map( 'sanitize_key', $contract['search_candidates'] ) ) ) )
                : array(),
            'runtime_reload_required' => ! empty( $contract['runtime_reload_required'] ),
        );
    }

    private function update_execution_contract_from_postconditions( $contract, $tool_name, $decoded_payload ) {
        if ( empty( $decoded_payload['postconditions'] ) || ! is_array( $decoded_payload['postconditions'] ) ) {
            return $contract;
        }

        foreach ( $decoded_payload['postconditions'] as $index => $postcondition ) {
            if ( ! is_array( $postcondition ) ) {
                continue;
            }

            $raw_id = isset( $postcondition['id'] ) ? (string) $postcondition['id'] : 'postcondition_' . intval( $index );
            $id     = 'postcondition_' . $this->sanitize_execution_contract_id( $raw_id );
            $label  = isset( $postcondition['label'] ) && '' !== trim( (string) $postcondition['label'] )
                ? trim( (string) $postcondition['label'] )
                : 'Required postcondition "' . $raw_id . '" must be true.';
            $ok     = array_key_exists( 'ok', $postcondition )
                ? ! empty( $postcondition['ok'] )
                : ( array_key_exists( 'success', $postcondition ) ? ! empty( $postcondition['success'] ) : false );

            if ( $ok ) {
                $contract = $this->complete_execution_contract_requirement( $contract, $id, $tool_name, $label );
            } else {
                $contract = $this->add_execution_contract_requirement(
                    $contract,
                    $id,
                    $label,
                    $tool_name,
                    array(
                        'kind' => 'postcondition',
                    )
                );
            }
        }

        return $contract;
    }

    private function add_execution_contract_requirement( $contract, $id, $label, $created_by = '', $meta = array() ) {
        $contract = $this->normalize_execution_contract( $contract );
        $id       = $this->sanitize_execution_contract_id( $id );
        if ( '' === $id || isset( $contract['completed'][ $id ] ) ) {
            return $contract;
        }

        $meta = is_array( $meta ) ? $meta : array();
        $contract['pending'][ $id ] = array(
            'id'         => $id,
            'label'      => $this->truncate_execution_contract_text( $label, 220 ),
            'created_by' => sanitize_key( (string) $created_by ),
            'kind'       => isset( $meta['kind'] ) ? sanitize_key( (string) $meta['kind'] ) : 'generic',
            'meta'       => $meta,
            'created_at' => time(),
        );

        return $contract;
    }

    private function complete_execution_contract_requirement( $contract, $id, $completed_by = '', $note = '' ) {
        $contract = $this->normalize_execution_contract( $contract );
        $id       = $this->sanitize_execution_contract_id( $id );
        if ( '' === $id ) {
            return $contract;
        }

        $entry = isset( $contract['pending'][ $id ] ) && is_array( $contract['pending'][ $id ] )
            ? $contract['pending'][ $id ]
            : array(
                'id'    => $id,
                'label' => '' !== trim( (string) $note ) ? trim( (string) $note ) : $id,
                'kind'  => 'generic',
                'meta'  => array(),
            );

        unset( $contract['pending'][ $id ] );

        $entry['completed_by'] = sanitize_key( (string) $completed_by );
        $entry['completed_at'] = time();
        if ( '' !== trim( (string) $note ) ) {
            $entry['note'] = $this->truncate_execution_contract_text( $note, 220 );
        }
        $contract['completed'][ $id ] = $entry;

        if ( count( $contract['completed'] ) > 30 ) {
            $contract['completed'] = array_slice( $contract['completed'], -30, null, true );
        }

        return $contract;
    }

    private function complete_loaded_tool_execution_requirement( $contract, $tool_name, $bridge = null ) {
        $contract  = $this->normalize_execution_contract( $contract );
        $tool_name = sanitize_key( (string) $tool_name );

        if ( '' === $tool_name || empty( $contract['pending']['execute_loaded_tools'] ) || ! is_array( $contract['pending']['execute_loaded_tools'] ) ) {
            return $contract;
        }

        $entry  = $contract['pending']['execute_loaded_tools'];
        $loaded = isset( $entry['meta']['loaded'] ) && is_array( $entry['meta']['loaded'] )
            ? array_values(
                array_filter(
                    array_map(
                        function ( $name ) {
                            return sanitize_key( (string) $name );
                        },
                        $entry['meta']['loaded']
                    )
                )
            )
            : array();

        if ( ! empty( $loaded ) && ! in_array( $tool_name, $loaded, true ) ) {
            return $contract;
        }

        $loaded_write_tools = array_values(
            array_filter(
                $loaded,
                function ( $name ) use ( $bridge ) {
                    return $this->is_write_tool_name_for_guard( $name, $bridge );
                }
            )
        );

        if ( ! empty( $loaded_write_tools ) && ! $this->is_write_tool_name_for_guard( $tool_name, $bridge ) ) {
            return $contract;
        }

        return $this->complete_execution_contract_requirement( $contract, 'execute_loaded_tools', $tool_name, 'Loaded tool was executed.' );
    }

    private function complete_execution_contract_requirements_by_kind( $contract, $kind, $completed_by = '', $note = '' ) {
        $contract = $this->normalize_execution_contract( $contract );
        $kind     = sanitize_key( (string) $kind );
        foreach ( $contract['pending'] as $id => $entry ) {
            if ( is_array( $entry ) && isset( $entry['kind'] ) && $kind === sanitize_key( (string) $entry['kind'] ) ) {
                $contract = $this->complete_execution_contract_requirement( $contract, $id, $completed_by, $note );
            }
        }

        return $contract;
    }

    private function complete_all_execution_contract_requirements( $contract, $completed_by = '', $note = '' ) {
        $contract = $this->normalize_execution_contract( $contract );
        foreach ( array_keys( $contract['pending'] ) as $id ) {
            $contract = $this->complete_execution_contract_requirement( $contract, $id, $completed_by, $note );
        }

        return $contract;
    }

    private function complete_execution_contract_fallbacks_for_tool( $contract, $tool_name ) {
        $contract  = $this->normalize_execution_contract( $contract );
        $tool_name = sanitize_key( (string) $tool_name );
        foreach ( $contract['pending'] as $id => $entry ) {
            if (
                is_array( $entry )
                && isset( $entry['kind'], $entry['meta']['tool'] )
                && 'fallback' === sanitize_key( (string) $entry['kind'] )
                && $tool_name === sanitize_key( (string) $entry['meta']['tool'] )
            ) {
                $contract = $this->complete_execution_contract_requirement( $contract, $id, $tool_name, 'Fallback tool completed.' );
            }
        }

        return $contract;
    }

    private function execution_contract_has_open_requirements( $contract ) {
        $contract = $this->normalize_execution_contract( $contract );
        return ! empty( $contract['pending'] );
    }

	private function execution_contract_has_open_kind( $contract, $kind ) {
		$contract = $this->normalize_execution_contract( $contract );
		$kind     = sanitize_key( (string) $kind );
		foreach ( $contract['pending'] as $entry ) {
			if ( is_array( $entry ) && $kind === sanitize_key( (string) ( $entry['kind'] ?? '' ) ) ) {
				return true;
			}
		}

		return false;
	}

    private function build_execution_contract_follow_up_message( $contract ) {
        $items = $this->describe_execution_contract_pending_requirements( $contract, 6 );
        $body  = ! empty( $items ) ? implode( "\n", $items ) : '- A required execution step is still pending.';

        return array(
            'role'    => 'system',
            'content' => '[EXECUTION CONTRACT]' . "\n"
                . 'Tool results opened durable requirements that are not complete yet:' . "\n"
                . $body . "\n"
                . 'Continue in this same turn. Use the required tool call or corrected arguments now. Do not give a final success answer until every requirement is complete. If a requirement is truly blocked, answer with the concrete blocker from tool output instead of claiming completion.',
        );
    }

    private function summarize_execution_contract( $contract ) {
        $contract = $this->normalize_execution_contract( $contract );

        return array(
            'pending_count'           => count( $contract['pending'] ),
            'completed_count'         => count( $contract['completed'] ),
            'runtime_reload_required' => ! empty( $contract['runtime_reload_required'] ),
            'pending'                 => $this->compact_execution_contract_entries( $contract['pending'], 12 ),
            'completed'               => $this->compact_execution_contract_entries( $contract['completed'], 12 ),
            'recent_events'           => array_slice( $contract['events'], -8 ),
        );
    }

    private function compact_execution_contract_entries( $entries, $limit = 12 ) {
        $entries = is_array( $entries ) ? array_slice( $entries, -1 * max( 1, intval( $limit ) ), null, true ) : array();
        $compact = array();

        foreach ( $entries as $id => $entry ) {
            if ( ! is_array( $entry ) ) {
                continue;
            }
            $compact[] = array(
                'id'           => isset( $entry['id'] ) ? (string) $entry['id'] : (string) $id,
                'kind'         => isset( $entry['kind'] ) ? (string) $entry['kind'] : '',
                'label'        => isset( $entry['label'] ) ? (string) $entry['label'] : '',
                'created_by'   => isset( $entry['created_by'] ) ? (string) $entry['created_by'] : '',
                'completed_by' => isset( $entry['completed_by'] ) ? (string) $entry['completed_by'] : '',
            );
        }

        return $compact;
    }

    private function mark_result_execution_incomplete( $result, $contract ) {
        $result = is_array( $result ) ? $result : array();
        $items  = $this->describe_execution_contract_pending_requirements( $contract, 5, false );
        $error  = 'Agent stopped before completing required execution steps.';
        if ( ! empty( $items ) ) {
            $error .= ' Pending: ' . implode( '; ', $items );
        }

        $result['execution_incomplete'] = true;
        $result['execution_policy_warning'] = $error;
        $result['success'] = false;
        $result['reply'] = '';
        $result['error'] = $error;
        $result['original_task_completed'] = false;

        return $result;
    }

    private function describe_execution_contract_pending_requirements( $contract, $limit = 6, $with_bullets = true ) {
        $contract = $this->normalize_execution_contract( $contract );
        $items    = array();
        $limit    = max( 1, intval( $limit ) );

        foreach ( $contract['pending'] as $entry ) {
            if ( ! is_array( $entry ) ) {
                continue;
            }
            $label = isset( $entry['label'] ) && '' !== trim( (string) $entry['label'] )
                ? trim( (string) $entry['label'] )
                : ( isset( $entry['id'] ) ? (string) $entry['id'] : 'pending requirement' );
            if ( ! empty( $entry['created_by'] ) ) {
                $label .= ' (opened by ' . sanitize_key( (string) $entry['created_by'] ) . ')';
            }
            $items[] = ( $with_bullets ? '- ' : '' ) . $this->truncate_execution_contract_text( $label, 240 );
            if ( count( $items ) >= $limit ) {
                break;
            }
        }

        return $items;
    }

    private function sanitize_execution_contract_id( $id ) {
        $id = strtolower( preg_replace( '/[^a-zA-Z0-9_\-]+/', '_', (string) $id ) );
        return trim( sanitize_key( $id ), '_' );
    }

    private function truncate_execution_contract_text( $text, $limit ) {
        $text  = trim( preg_replace( '/\s+/', ' ', (string) $text ) );
        $limit = max( 40, intval( $limit ) );
        if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) {
            return mb_strlen( $text ) > $limit ? mb_substr( $text, 0, $limit - 3 ) . '...' : $text;
        }

        return strlen( $text ) > $limit ? substr( $text, 0, $limit - 3 ) . '...' : $text;
    }

    private function is_tool_management_name( $tool_name ) {
        $tool_name = sanitize_key( (string) $tool_name );
        if ( '' === $tool_name ) {
            return true;
        }

        if ( 'task_checkpoint' === $tool_name ) {
            return true;
        }

        if ( 0 === strpos( $tool_name, 'tool_' ) ) {
            return true;
        }

        return in_array(
            $tool_name,
            array(
                'session_status_get',
                'founder_notes_get',
                'workspace_bootstrap_get',
            ),
            true
        );
    }

    /**
     * A successful concrete tool proves that the current task path exists.
     * Discovery, approval, and asynchronous delegation only choose a path and
     * therefore cannot clear a capability-gap obligation by themselves.
     */
    private function is_capability_gap_resolution_tool( $tool_name ) {
        $tool_name = sanitize_key( (string) $tool_name );
        if ( $this->is_tool_management_name( $tool_name ) ) {
            return false;
        }

        return ! in_array(
            $tool_name,
            array(
                'extension_request_create',
                'extension_approval_record',
                'skill_search',
                'external_skill_search',
                'external_skill_get',
                'subagent_spawn',
                'sessions_spawn',
                'sessions_send',
            ),
            true
        );
    }

    private function tool_payload_owns_terminal_lifecycle( $tool_name, $decoded_payload, $tool_succeeded ) {
        if ( empty( $tool_succeeded ) ) {
            return false;
        }
        $tool_name       = sanitize_key( (string) $tool_name );
        $decoded_payload = is_array( $decoded_payload ) ? $decoded_payload : array();

        return ! empty( $decoded_payload['terminal_after_tool'] )
            || ! empty( $decoded_payload['agent_loop_terminal'] )
            || 'main_coding_workspace_apply' === $tool_name;
    }

    private function is_write_tool_name_for_guard( $tool_name, $bridge = null ) {
        $tool_name = sanitize_key( (string) $tool_name );
        if ( '' === $tool_name ) {
            return false;
        }
		if ( $this->is_tool_management_name( $tool_name ) ) {
			return false;
		}

        if ( is_object( $bridge ) && method_exists( $bridge, 'is_mcp_runtime_write_tool' ) ) {
            return ! empty( $bridge->is_mcp_runtime_write_tool( $tool_name ) );
        }

        if ( 'code_read' === $tool_name || preg_match( '/^(tool_|memory_|founder_notes_get$|workspace_bootstrap_get$|session_status_get$|rollback_last_action$|woo_product_lookup$|wp_search_posts$|read_post_field$|search_text_in_post$|code_search$|file_read$|file_list$|batch_read$|site_image_lookup$|image_analyze$)/', $tool_name ) ) {
            return false;
        }

        if ( in_array( $tool_name, array( 'workspace_bootstrap_write', 'session_note_add', 'companion_memory_capture', 'skill_create', 'skill_update' ), true ) ) {
            return false;
        }

        // Explicit write tools whose names do not start with a mutating prefix
        // (e.g. no "add_"/"move_" branch in the regex below). Without these the
        // execution guard never registers an outlet/variation-add as a real
        // mutation, so a turn that loads them can silently end without writing.
        if ( in_array( $tool_name, array( 'move_to_outlet', 'add_product_variation', 'inventory_cleanup', 'clone_partner_variable_product_from_url' ), true ) ) {
            return true;
        }

        return (bool) preg_match(
            '/^(create_|update_|publish_|delete_|remove_|trash_|restore_|set_|bulk_|replace_|swap_|manage_|upload_|send_|schedule_|cache_flush$|elementor_update_)/',
            $tool_name
        );
    }

    /**
     * Separate write capability from verified mutation evidence.
     *
     * A generic shell is write-capable for authorization purposes, but a
     * successful read-only inspection must not be reported as a successful
     * mutation. Shell file writes are evidence-backed by the executor's
     * resolved write_targets ledger; database changes must use a dedicated
     * mutation tool with an explicit result.
     */
    private function is_verified_write_execution( $tool_name, $decoded_payload, $bridge = null ) {
        $tool_name       = sanitize_key( (string) $tool_name );
        $decoded_payload = is_array( $decoded_payload ) ? $decoded_payload : array();
        if ( ! $this->is_write_tool_name_for_guard( $tool_name, $bridge ) ) {
            return false;
        }

        if ( 'main_coding_shell_run' === $tool_name ) {
            return ! empty( $decoded_payload['write_targets'] ) && is_array( $decoded_payload['write_targets'] );
        }

        return true;
    }

    /**
     * Wrap the progress callback so long-running turns can refresh their lane.
     *
     * @param string        $session_id        Session identifier.
     * @param int           $lock_ttl          Desired lane TTL.
     * @param callable|null $progress_callback Original callback.
     * @return callable|null
     */
    private function wrap_progress_callback( $session_id, $lock_ttl, $progress_callback ) {
        $session_id   = trim( (string) $session_id );
        $lock_ttl     = max( 60, intval( $lock_ttl ) );
		$can_refresh  = '' !== $session_id
			&& $this->execution_lane
			&& method_exists( $this->execution_lane, 'refresh_session_lane' );

        if ( ! is_callable( $progress_callback ) && ! $can_refresh ) {
            return null;
        }

		return function ( $event = '', $payload = array() ) use ( $progress_callback, $session_id, $lock_ttl, $can_refresh ) {
			if ( $can_refresh ) {
				$this->execution_lane->refresh_session_lane( $session_id, $lock_ttl );
            }

            $this->emit_progress_callback( $progress_callback, $event, $payload );
        };
    }

    private function emit_progress_callback( $callback, $event, $payload = array() ) {
        if ( ! is_callable( $callback ) ) {
            return;
        }

        call_user_func( $callback, (string) $event, is_array( $payload ) ? $payload : array() );
    }
}
