#!/bin/bash
set -euo pipefail

CODEX_LINUX_APP_ID=codex-desktop
CODEX_LINUX_APP_DISPLAY_NAME=ChatGPT
CODEX_LINUX_WEBVIEW_PORT=${CODEX_WEBVIEW_PORT:-5175}
codex_capture_original_ld_library_path() {
    # Packaged runtimes may extend LD_LIBRARY_PATH after this point. Main-process
    # host children use this three-state snapshot instead of inheriting those paths.
    # HOST is populated later from the login shell; inherited values are stale or
    # user-controlled internal state and must never override this fresh snapshot.
    unset CODEX_LINUX_HOST_LD_LIBRARY_PATH_STATE CODEX_LINUX_HOST_LD_LIBRARY_PATH_VALUE
    if [ "${LD_LIBRARY_PATH+x}" != x ]; then
        CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_STATE="unset"
        CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_VALUE=""
    elif [ -z "$LD_LIBRARY_PATH" ]; then
        CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_STATE="empty"
        CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_VALUE=""
    else
        CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_STATE="value"
        CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_VALUE="$LD_LIBRARY_PATH"
    fi
    export CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_STATE CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_VALUE
}

codex_restore_original_ld_library_path() {
    local state="${CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_STATE:-}"
    local value="${CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_VALUE:-}"

    unset CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_STATE CODEX_LINUX_ORIGINAL_LD_LIBRARY_PATH_VALUE
    unset CODEX_LINUX_HOST_LD_LIBRARY_PATH_STATE CODEX_LINUX_HOST_LD_LIBRARY_PATH_VALUE
    case "$state" in
        unset) unset LD_LIBRARY_PATH ;;
        empty) export LD_LIBRARY_PATH="" ;;
        value) export LD_LIBRARY_PATH="$value" ;;
    esac
}

codex_exec_host_command() {
    codex_restore_original_ld_library_path
    exec "$@"
}

codex_run_host_command() (
    codex_exec_host_command "$@"
)

# Capture before package-specific launcher patches add application runtime paths.
codex_capture_original_ld_library_path

resolve_script_dir() {
    local source="${BASH_SOURCE[0]}"
    local dir

    while [ -L "$source" ]; do
        dir="$(cd -P "$(dirname "$source")" && pwd)"
        source="$(readlink "$source")"
        case "$source" in
            /*) ;;
            *) source="$dir/$source" ;;
        esac
    done

    cd -P "$(dirname "$source")" && pwd
}

SCRIPT_DIR="$(resolve_script_dir)"
if [ -z "${CODEX_HOME:-}" ]; then
    if [ -n "${HOME:-}" ]; then
        CODEX_HOME="$HOME/.codex"
    else
        CODEX_HOME=""
    fi
fi
WEBVIEW_DIR="$SCRIPT_DIR/content/webview"
LOG_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/$CODEX_LINUX_APP_ID"
LOG_FILE="$LOG_DIR/launcher.log"
APP_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/$CODEX_LINUX_APP_ID"
APP_SETTINGS_FILE="$APP_CONFIG_DIR/settings.json"
USER_ELECTRON_FLAGS_FILE="$APP_CONFIG_DIR/electron-flags.conf"
CODEX_LINUX_SETTINGS_FILE="$APP_SETTINGS_FILE"
CODEX_LINUX_FEATURES_DIR="$SCRIPT_DIR/.codex-linux/features"
export CODEX_HOME CODEX_LINUX_APP_ID CODEX_LINUX_APP_DISPLAY_NAME CODEX_LINUX_WEBVIEW_PORT CODEX_LINUX_SETTINGS_FILE CODEX_LINUX_FEATURES_DIR
APP_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/$CODEX_LINUX_APP_ID"

configure_runtime_tmpdir() {
    [ -z "${TMPDIR:-}" ] || return 0

    if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -d "$XDG_RUNTIME_DIR" ] && [ -w "$XDG_RUNTIME_DIR" ]; then
        TMPDIR="$XDG_RUNTIME_DIR/$CODEX_LINUX_APP_ID/tmp"
    else
        TMPDIR="$APP_STATE_DIR/tmp"
    fi
    mkdir -p "$TMPDIR"
    chmod 700 "$TMPDIR"
    export TMPDIR
}

configure_runtime_tmpdir
APP_PID_FILE="$APP_STATE_DIR/app.pid"
WEBVIEW_PID_FILE="$APP_STATE_DIR/webview.pid"
LAUNCH_ACTION_RUNTIME_DIR="${XDG_RUNTIME_DIR:-$APP_STATE_DIR}/$CODEX_LINUX_APP_ID"
LAUNCH_ACTION_SOCKET="$LAUNCH_ACTION_RUNTIME_DIR/launch-action.sock"
PACKAGED_RUNTIME_HELPER="$SCRIPT_DIR/.codex-linux/codex-packaged-runtime.sh"
COLD_START_HOOK_DIR="$SCRIPT_DIR/.codex-linux/cold-start.d"
FEATURE_ENV_DIR="$SCRIPT_DIR/.codex-linux/env.d"
FEATURE_PRELAUNCH_HOOK_DIR="$SCRIPT_DIR/.codex-linux/prelaunch.d"
FEATURE_ELECTRON_ARGS_DIR="$SCRIPT_DIR/.codex-linux/electron-args.d"
FEATURE_LAUNCHER_HOOK_DIR="$SCRIPT_DIR/.codex-linux/launcher.d"
FEATURE_AFTER_EXIT_HOOK_DIR="$SCRIPT_DIR/.codex-linux/after-exit.d"
MANAGED_NODE_BIN_DIR="$SCRIPT_DIR/resources/node-runtime/bin"
APP_NOTIFICATION_ICON_NAME="$CODEX_LINUX_APP_ID"
APP_NOTIFICATION_ICON_BUNDLE="$SCRIPT_DIR/.codex-linux/$APP_NOTIFICATION_ICON_NAME.png"
APP_NOTIFICATION_ICON_SYSTEM="/usr/share/icons/hicolor/256x256/apps/$APP_NOTIFICATION_ICON_NAME.png"
APP_NOTIFICATION_ICON_REPO="$SCRIPT_DIR/../assets/codex.png"
MULTI_LAUNCH_REQUESTED=0
MULTI_LAUNCH_ACTIVE=0
CODEX_LINUX_INSTANCE_ID=""
LAUNCHER_ARGS=()

# Internal marker consumed by patched Electron bundles; never trust inheritance.
unset CODEX_LINUX_MULTI_LAUNCH

early_truthy_env_value() {
    case "${1:-}" in
        1|true|TRUE|yes|YES|on|ON) return 0 ;;
        *) return 1 ;;
    esac
}

normalize_tcp_port() {
    local name="$1"
    local port="$2"
    local value

    case "$port" in
        ""|*[!0-9]*)
            echo "$name must be a TCP port number" >&2
            return 1
            ;;
    esac
    value="$port"
    while [ "${value#0}" != "$value" ]; do
        value="${value#0}"
    done
    [ -n "$value" ] || value=0

    if [ "${#value}" -gt 5 ] || [ "$value" -lt 1 ] || [ "$value" -gt 65535 ]; then
        echo "$name must be between 1 and 65535" >&2
        return 1
    fi

    printf '%s\n' "$value"
}

validate_tcp_port() {
    normalize_tcp_port "$1" "$2" >/dev/null
}

if ! CODEX_LINUX_WEBVIEW_PORT="$(normalize_tcp_port "CODEX_WEBVIEW_PORT" "$CODEX_LINUX_WEBVIEW_PORT")"; then
    exit 1
fi

launcher_port_is_open() {
    local port="$1"
    ( exec 3<>/dev/tcp/127.0.0.1/"$port" ) 2>/dev/null
}

choose_multi_launch_port() {
    local range="${CODEX_MULTI_LAUNCH_PORT_RANGE:-$CODEX_LINUX_WEBVIEW_PORT-$((CODEX_LINUX_WEBVIEW_PORT + 4))}"
    local start end port

    case "$range" in
        *-*)
            start="${range%-*}"
            end="${range#*-}"
            ;;
        *)
            start="$range"
            end="$range"
            ;;
    esac

    start="$(normalize_tcp_port "CODEX_MULTI_LAUNCH_PORT_RANGE start" "$start")" || return 1
    end="$(normalize_tcp_port "CODEX_MULTI_LAUNCH_PORT_RANGE end" "$end")" || return 1
    if [ "$start" -gt "$end" ]; then
        echo "CODEX_MULTI_LAUNCH_PORT_RANGE start must be <= end" >&2
        return 1
    fi

    for port in $(seq "$start" "$end"); do
        if ! launcher_port_is_open "$port"; then
            echo "$port"
            return 0
        fi
    done

    echo "No free ChatGPT Desktop webview port found in CODEX_MULTI_LAUNCH_PORT_RANGE=$range" >&2
    return 1
}

parse_launcher_args() {
    local passthrough=0

    LAUNCHER_ARGS=()
    if early_truthy_env_value "${CODEX_MULTI_LAUNCH:-}"; then
        MULTI_LAUNCH_REQUESTED=1
    fi

    while [ "$#" -gt 0 ]; do
        if [ "$passthrough" -eq 1 ]; then
            LAUNCHER_ARGS+=("$1")
            shift
            continue
        fi

        case "$1" in
            --)
                passthrough=1
                LAUNCHER_ARGS+=("$1")
                ;;
            --new-instance|--multi-instance|--multi-launch)
                MULTI_LAUNCH_REQUESTED=1
                ;;
            *)
                LAUNCHER_ARGS+=("$1")
                ;;
        esac
        shift
    done
}

configure_multi_launch_instance() {
    parse_launcher_args "$@"
    [ "$MULTI_LAUNCH_REQUESTED" -eq 1 ] || return 0

    local base_state_dir="$APP_STATE_DIR"
    local selected_port

    selected_port="$(choose_multi_launch_port)" || exit 1
    CODEX_LINUX_WEBVIEW_PORT="$selected_port"
    CODEX_LINUX_INSTANCE_ID="port-$CODEX_LINUX_WEBVIEW_PORT"
    MULTI_LAUNCH_ACTIVE=1
    CODEX_LINUX_MULTI_LAUNCH=1

    APP_STATE_DIR="$base_state_dir/instances/$CODEX_LINUX_INSTANCE_ID"
    APP_PID_FILE="$APP_STATE_DIR/app.pid"
    WEBVIEW_PID_FILE="$APP_STATE_DIR/webview.pid"
    if [ -n "${XDG_RUNTIME_DIR:-}" ]; then
        LAUNCH_ACTION_RUNTIME_DIR="$XDG_RUNTIME_DIR/$CODEX_LINUX_APP_ID/instances/$CODEX_LINUX_INSTANCE_ID"
    else
        LAUNCH_ACTION_RUNTIME_DIR="$APP_STATE_DIR"
    fi
    LAUNCH_ACTION_SOCKET="$LAUNCH_ACTION_RUNTIME_DIR/launch-action.sock"
    LOG_FILE="$LOG_DIR/launcher-$CODEX_LINUX_INSTANCE_ID.log"

    if [ -n "${CODEX_ELECTRON_USER_DATA_DIR:-}" ]; then
        CODEX_ELECTRON_USER_DATA_DIR="$CODEX_ELECTRON_USER_DATA_DIR/instances/$CODEX_LINUX_INSTANCE_ID"
    else
        CODEX_ELECTRON_USER_DATA_DIR="$APP_STATE_DIR/electron-user-data"
    fi
    export CODEX_ELECTRON_USER_DATA_DIR CODEX_LINUX_INSTANCE_ID CODEX_LINUX_MULTI_LAUNCH CODEX_LINUX_WEBVIEW_PORT
}

configure_multi_launch_instance "$@"
WEBVIEW_ORIGIN="http://127.0.0.1:$CODEX_LINUX_WEBVIEW_PORT"

mkdir -p "$LOG_DIR" "$APP_CONFIG_DIR" "$APP_STATE_DIR" "$LAUNCH_ACTION_RUNTIME_DIR"
chmod 700 "$LAUNCH_ACTION_RUNTIME_DIR" 2>/dev/null || true
export CODEX_DESKTOP_LAUNCH_ACTION_SOCKET="$LAUNCH_ACTION_SOCKET"
STARTED_WEBVIEW_PID=""
ADOPTED_WEBVIEW_PID=""
ELECTRON_PID=""
RUNNING_APP_PID=""
WARM_START=0
LAUNCHER_LOCK_HELD=0
LAUNCHER_LOCK_TIMED_OUT=0
LAUNCHER_LOCK_HELPER_PID=""
LAUNCHER_LOCK_HELPER_START_TIME=""
LAUNCHER_LOCK_STATUS_PATH=""

if [[ "${LAUNCHER_ARGS[0]:-}" == "--help" || "${LAUNCHER_ARGS[0]:-}" == "-h" ]]; then
    cat <<HELP
Usage: ./start.sh [OPTIONS] [-- ELECTRON_FLAGS...]

Launches the $CODEX_LINUX_APP_DISPLAY_NAME app.

Options:
  -h, --help                  Show this help message and exit
  --new-instance              Start a separate app instance on the first free port in the multi-launch range
  --new-chat                  Open the main window on a new chat
  --quick-chat                Open a projectless quick chat
  --prompt-chat               Show the compact prompt for a new chat
  --hotkey-window             Alias for --prompt-chat
  --safe-mode                 X11 + software rendering fallback
  --disable-gpu               Disable Electron GPU acceleration
  --enable-gpu                Re-enable Electron GPU acceleration
  --x11                       Force X11/XWayland
  --wayland                   Force native Wayland
  --diagnose-scaling          Print display/session scaling diagnostics and exit

Default launch keeps Electron GPU enabled and lets Electron choose the platform.
Extra flags are passed directly to Electron.
Persistent launch flags: $USER_ELECTRON_FLAGS_FILE

Environment:
  CODEX_OZONE_PLATFORM=x11|wayland|auto
                              Select the Chromium Ozone backend without editing flags;
                              explicit --x11/--wayland/--ozone-platform* flags win
  CODEX_FORCE_DEVICE_SCALE_FACTOR=N
                              Pass --force-device-scale-factor=N to Electron (e.g. 1, 1.5, 2);
                              use 1 if the UI is oversized on HiDPI/fractional-scaling setups
  CODEX_LINUX_RENDERING_MODE=auto|default|wslg|wayland-gpu
                              Auto-detect WSLg, keep generic Linux defaults, force the WSLg profile,
                              or force native Wayland with GPU compositing enabled
  CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=0|1
                              Override Wayland side-panel compositing workaround; 1 forces and 0 disables
  CODEX_ELECTRON_DISABLE_DEV_SHM_USAGE=auto|0|1
                              Override --disable-dev-shm-usage; auto keeps it only when /dev/shm is missing or small
  CODEX_LAUNCHER_LOCK_WAIT_SECONDS=N
                              Seconds to wait for another launcher to leave the startup lock (default: 5)
  CODEX_FORCE_RENDERER_ACCESSIBILITY=auto|0|1
                              Override --force-renderer-accessibility; auto forces it only when assistive
                              technology is detected and never under WSLg or native wayland-gpu
  CODEX_MULTI_LAUNCH=1        Treat normal launches like --new-instance
  CODEX_MULTI_LAUNCH_PORT_RANGE=START-END
                              Port allocation range for --new-instance (default: CODEX_WEBVIEW_PORT through +4)

Logs: $LOG_FILE
HELP
    exit 0
fi

# Capture interactivity before stdout/stderr are redirected into the log;
# [ -t 1 ] is always false after the exec below.
LAUNCHER_INTERACTIVE_TERMINAL=0
if [ -t 0 ] && [ -t 1 ]; then
    LAUNCHER_INTERACTIVE_TERMINAL=1
fi
LAUNCHER_EARLY_STDERR_FD=""
if exec {LAUNCHER_EARLY_STDERR_FD}>&2 2>/dev/null; then
    :
else
    LAUNCHER_EARLY_STDERR_FD=""
fi

LAUNCHER_DIAGNOSE_SCALING=0
if [[ "${LAUNCHER_ARGS[0]:-}" == "--diagnose-scaling" ]]; then
    # Diagnostics are read interactively; keep them on the terminal instead
    # of the launcher log. The early-stderr capture above left fd 2 on
    # /dev/null, so point it back at the saved terminal stderr.
    LAUNCHER_DIAGNOSE_SCALING=1
    if [ -n "${LAUNCHER_EARLY_STDERR_FD:-}" ]; then
        exec 2>&"$LAUNCHER_EARLY_STDERR_FD"
    fi
else
    exec >>"$LOG_FILE" 2>&1
fi

echo "[$(date -Is)] Starting $CODEX_LINUX_APP_DISPLAY_NAME launcher"
if [ "$MULTI_LAUNCH_ACTIVE" -eq 1 ]; then
    echo "Multi-launch instance: id=$CODEX_LINUX_INSTANCE_ID webview_port=$CODEX_LINUX_WEBVIEW_PORT state_dir=$APP_STATE_DIR user_data_dir=$CODEX_ELECTRON_USER_DATA_DIR"
fi

now_ms() {
    local value seconds nanos
    value="$(date +%s%N 2>/dev/null || true)"
    case "$value" in
        *N*|"") echo "$(($(date +%s) * 1000))" ;;
        *)
            seconds="${value:0:${#value}-9}"
            nanos="${value: -9}"
            echo "$((seconds * 1000 + 10#$nanos / 1000000))"
            ;;
    esac
}

LAUNCHER_START_MS="$(now_ms)"

log_phase() {
    local phase="$1"
    local elapsed_ms
    elapsed_ms="$(($(now_ms) - LAUNCHER_START_MS))"
    echo "[$(date -Is)] launcher_phase=$phase elapsedMs=$elapsed_ms"
}

launcher_notice() {
    local message="$1"

    echo "$message"
    if [ -n "${LAUNCHER_EARLY_STDERR_FD:-}" ]; then
        printf '%s\n' "$message" 2>/dev/null 1>&"$LAUNCHER_EARLY_STDERR_FD" || true
    fi
}

make_tree_owner_writable() {
    local path="$1"

    [ -e "$path" ] || return 0
    chmod -R u+rwX "$path" 2>/dev/null || true
}

make_path_owner_trusted() {
    local path

    for path in "$@"; do
        [ ! -L "$path" ] || return 1
        [ -e "$path" ] || continue
        chmod u+rwX,go-w "$path" 2>/dev/null || return 1
    done
}

make_tree_owner_trusted() {
    local path="$1"

    [ ! -L "$path" ] || return 1
    [ -e "$path" ] || return 0
    if find "$path" -xdev -type l -print -quit 2>/dev/null | grep -q .; then
        return 1
    fi
    chmod -R u+rwX,go-w "$path" 2>/dev/null || return 1
}

path_has_unsafe_write() {
    local path

    for path in "$@"; do
        if [ -L "$path" ]; then
            return 0
        fi
        [ -e "$path" ] || continue
        if find "$path" -maxdepth 0 -perm /022 -print -quit 2>/dev/null | grep -q .; then
            return 0
        fi
    done
    return 1
}

tree_has_unsafe_write() {
    local path="$1"

    [ ! -L "$path" ] || return 0
    [ -e "$path" ] || return 1
    find "$path" -xdev \( -type l -o -perm /022 \) -print -quit 2>/dev/null | grep -q .
}

remove_tree_if_exists() {
    local path="$1"

    [ -e "$path" ] || return 0
    make_tree_owner_writable "$path"
    rm -rf "$path"
}

replace_symlink() {
    local target="$1"
    local link_path="$2"

    if [ -e "$link_path" ] && [ ! -L "$link_path" ]; then
        remove_tree_if_exists "$link_path"
    fi

    if ! ln -sfnT "$target" "$link_path" 2>/dev/null; then
        remove_tree_if_exists "$link_path"
        ln -sfnT "$target" "$link_path"
    fi
}

fix_bundled_marketplace_tmp_permissions() {
    local codex_home="${CODEX_HOME:-$HOME/.codex}"
    local marketplace_tmp_root="$codex_home/.tmp/bundled-marketplaces"

    [ -d "$marketplace_tmp_root" ] || return 0
    find "$marketplace_tmp_root" -type d -exec chmod u+rwx {} + 2>/dev/null || true
    find "$marketplace_tmp_root" -type f -exec chmod u+rw {} + 2>/dev/null || true
}

clear_bundled_marketplace_tmp_cache() {
    local codex_home="${CODEX_HOME:-$HOME/.codex}"
    local marketplace_tmp_root="$codex_home/.tmp/bundled-marketplaces"
    local path

    fix_bundled_marketplace_tmp_permissions
    for path in "$marketplace_tmp_root"/openai-bundled "$marketplace_tmp_root"/openai-bundled.staging-*; do
        [ -e "$path" ] || continue
        remove_tree_if_exists "$path"
    done
}

import_graphical_env_entry() {
    local entry="$1"
    local name="${entry%%=*}"

    [ "$entry" != "$name" ] || return 0

    # SOMMELIER_VERSION / SOMMELIER_VM_IDENTIFIER are imported here so the
    # ChromeOS / Crostini auto-X11 fallback in set_electron_defaults sees
    # them on app-launcher paths where they may only be present in the
    # systemd --user manager environment (and not in the spawned process's
    # initial env).
    case "$name" in
        DISPLAY|WAYLAND_DISPLAY|XDG_SESSION_TYPE|XDG_CURRENT_DESKTOP|XDG_SESSION_DESKTOP|DBUS_SESSION_BUS_ADDRESS|XDG_RUNTIME_DIR|DESKTOP_SESSION|XAUTHORITY|HYPRLAND_INSTANCE_SIGNATURE|YDOTOOL_SOCKET|SOMMELIER_VERSION|SOMMELIER_VM_IDENTIFIER)
            if [ -z "${!name:-}" ]; then
                export "$entry"
            fi
            ;;
    esac
}

import_graphical_env_from_proc() {
    local pid="$1"
    local entry
    local found_display=0

    [[ "$pid" =~ ^[0-9]+$ ]] || return 1
    [ -r "/proc/$pid/environ" ] || return 1

    while IFS= read -r -d '' entry; do
        case "$entry" in
            DISPLAY=*|WAYLAND_DISPLAY=*) found_display=1 ;;
        esac
        import_graphical_env_entry "$entry"
    done < "/proc/$pid/environ"

    [ "$found_display" -eq 1 ]
}

import_graphical_env_from_systemd_user() {
    local entry
    local found_display=0

    command -v systemctl >/dev/null 2>&1 || return 1

    while IFS= read -r entry; do
        case "$entry" in
            DISPLAY=*|WAYLAND_DISPLAY=*) found_display=1 ;;
        esac
        import_graphical_env_entry "$entry"
    done < <(systemctl --user show-environment 2>/dev/null || true)

    [ "$found_display" -eq 1 ]
}

discover_graphical_env_from_processes() {
    local status_file
    local pid
    local uid

    for status_file in /proc/[0-9]*/status; do
        [ -e "$status_file" ] || continue
        pid="${status_file#/proc/}"
        pid="${pid%/status}"
        uid="$(awk '/^Uid:/ {print $2}' "$status_file" 2>/dev/null || true)"
        [ "$uid" = "$(id -u)" ] || continue
        import_graphical_env_from_proc "$pid" && return 0
    done

    return 1
}

hydrate_graphical_session_env() {
    if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then
        # Display vars are already present, but the ChromeOS / Crostini
        # auto-X11 fallback in set_electron_defaults also needs
        # SOMMELIER_VERSION. On some app-launcher paths the spawned
        # process inherits DISPLAY / WAYLAND_DISPLAY but not the Sommelier
        # markers (those live only in the systemd --user manager env). Try
        # one user-manager import in that case so the whitelisted
        # SOMMELIER_VERSION / SOMMELIER_VM_IDENTIFIER get hydrated.
        if [ -z "${SOMMELIER_VERSION:-}" ]; then
            import_graphical_env_from_systemd_user >/dev/null 2>&1 || true
        fi
        return 0
    fi

    import_graphical_env_from_systemd_user && return 0
    discover_graphical_env_from_processes && return 0
    return 0
}

desktop_entry_exists() {
    local desktop_name="$CODEX_LINUX_APP_ID.desktop"
    local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
    local data_dirs="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
    local data_dir
    local -a data_dirs_array

    [ -f "$data_home/applications/$desktop_name" ] && return 0

    IFS=: read -r -a data_dirs_array <<< "$data_dirs"
    for data_dir in "${data_dirs_array[@]}"; do
        [ -f "$data_dir/applications/$desktop_name" ] && return 0
    done

    return 1
}

register_url_scheme_handlers() {
    command -v xdg-mime >/dev/null 2>&1 || return 0
    desktop_entry_exists || return 0

    local desktop_name="$CODEX_LINUX_APP_ID.desktop"
    local scheme
    local mime_type
    local current_handler

    for scheme in codex codex-browser-sidebar; do
        mime_type="x-scheme-handler/$scheme"
        current_handler="$(xdg-mime query default "$mime_type" 2>/dev/null || true)"
        [ "$current_handler" = "$desktop_name" ] && continue
        xdg-mime default "$desktop_name" "$mime_type" >/dev/null 2>&1 || true
    done
}

linux_setting_enabled() {
    local key="$1"
    local default_value="${2:-1}"

    python3 - "$APP_SETTINGS_FILE" "$key" "$default_value" <<'PY'
import json
import sys

settings_path, key, default_value = sys.argv[1:4]
enabled = default_value == "1"

try:
    with open(settings_path, "r", encoding="utf-8") as handle:
        data = json.load(handle)
    if isinstance(data, dict) and key in data:
        value = data[key]
        if isinstance(value, bool):
            enabled = value
        elif isinstance(value, (int, float)):
            enabled = value != 0
        elif isinstance(value, str):
            enabled = value.strip().lower() not in {"0", "false", "no", "off"}
except FileNotFoundError:
    pass
except (OSError, json.JSONDecodeError):
    pass

raise SystemExit(0 if enabled else 1)
PY
}

load_packaged_runtime_helper() {
    if [ -f "$PACKAGED_RUNTIME_HELPER" ]; then
        # shellcheck disable=SC1090
        . "$PACKAGED_RUNTIME_HELPER"
    fi
}

run_packaged_runtime_prelaunch() {
    if declare -F codex_packaged_runtime_prelaunch >/dev/null 2>&1; then
        codex_packaged_runtime_prelaunch
    fi
}

export_packaged_runtime_env() {
    if declare -F codex_packaged_runtime_export_env >/dev/null 2>&1; then
        codex_packaged_runtime_export_env
    fi
}

path_without_entry() {
    local path_value="$1"
    local entry_to_remove="$2"
    local remaining
    local entry
    local result=""
    local separator=""

    remaining="$path_value:"
    while [ -n "$remaining" ]; do
        entry="${remaining%%:*}"
        remaining="${remaining#*:}"
        if [ "$entry" != "$entry_to_remove" ]; then
            result="$result$separator$entry"
            separator=:
        fi
    done

    printf '%s\n' "$result"
}

prepend_managed_node_runtime_to_path() {
    [ -x "$MANAGED_NODE_BIN_DIR/node" ] || return 0

    local current_path="${PATH:-}"
    CODEX_LINUX_USER_PATH="$(path_without_entry "${CODEX_LINUX_USER_PATH-$current_path}" "$MANAGED_NODE_BIN_DIR")"
    export CODEX_LINUX_USER_PATH

    case "$current_path" in
        "$MANAGED_NODE_BIN_DIR"|"$MANAGED_NODE_BIN_DIR":*) ;;
        *) export PATH="$MANAGED_NODE_BIN_DIR${current_path:+:$current_path}" ;;
    esac
    export CODEX_MANAGED_NODE_RUNTIME_DIR="$SCRIPT_DIR/resources/node-runtime"
}

source_feature_env_files() {
    [ -n "${FEATURE_ENV_DIR:-}" ] || return 0
    [ -d "$FEATURE_ENV_DIR" ] || return 0

    local env_file
    local line
    local name
    local value
    for env_file in "$FEATURE_ENV_DIR"/*; do
        [ -f "$env_file" ] || continue
        echo "Loading Linux feature environment: $env_file"
        while IFS= read -r line || [ -n "$line" ]; do
            line="${line%$'\r'}"
            case "$line" in
                ""|\#*) continue ;;
                *=*) ;;
                *)
                    echo "Ignoring Linux feature env line without KEY=VALUE in $env_file"
                    continue
                    ;;
            esac
            name="${line%%=*}"
            value="${line#*=}"
            case "$name" in
                ""|[!A-Za-z_]*|*[!A-Za-z0-9_]*)
                    echo "Ignoring Linux feature env line with invalid key '$name' in $env_file"
                    continue
                    ;;
            esac
            export "$name=$value"
        done < "$env_file"
    done
}

run_feature_prelaunch_hooks() {
    [ -n "${FEATURE_PRELAUNCH_HOOK_DIR:-}" ] || return 0
    [ -d "$FEATURE_PRELAUNCH_HOOK_DIR" ] || return 0

    local hook
    for hook in "$FEATURE_PRELAUNCH_HOOK_DIR"/*; do
        [ -f "$hook" ] || continue
        [ -x "$hook" ] || continue
        echo "Running Linux feature prelaunch hook: $hook"
        CODEX_HOME="$CODEX_HOME" \
        CODEX_LINUX_APP_DIR="$SCRIPT_DIR" \
        CODEX_LINUX_APP_STATE_DIR="$APP_STATE_DIR" \
        CODEX_LINUX_FEATURES_DIR="$CODEX_LINUX_FEATURES_DIR" \
        CODEX_LINUX_LAUNCHER_LOG="$LOG_FILE" \
        CODEX_LINUX_LAUNCHER_CMD="${CODEX_LINUX_LAUNCHER_CMD:-$SCRIPT_DIR/start.sh}" \
        CODEX_LINUX_FEATURE_HOOK_PHASE="prelaunch" \
        CODEX_UPDATE_MANAGER_PATH="${CODEX_UPDATE_MANAGER_PATH:-}" \
            codex_run_host_command "$hook" "$SCRIPT_DIR" "$APP_STATE_DIR" "$LOG_DIR"
    done
}

bundled_plugin_version() {
    local plugin_json="$1"

    python3 - "$plugin_json" <<'PY'
import json
import re
import sys

try:
    with open(sys.argv[1], "r", encoding="utf-8") as handle:
        version = json.load(handle).get("version")
except (OSError, json.JSONDecodeError):
    version = None

if not isinstance(version, str) or not version.strip():
    raise SystemExit(1)
version = version.strip()
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._+-]{0,127}", version) is None:
    raise SystemExit(1)
print(version)
PY
}

bundled_plugin_name() {
    local plugin_json="$1"

    python3 - "$plugin_json" <<'PY'
import json
import re
import sys

try:
    with open(sys.argv[1], "r", encoding="utf-8") as handle:
        name = json.load(handle).get("name")
except (OSError, json.JSONDecodeError):
    name = None

if not isinstance(name, str) or re.fullmatch(r"[A-Za-z0-9._-]+", name.strip()) is None:
    raise SystemExit(1)
print(name.strip())
PY
}

# The bundled marketplace metadata is shared by every plugin sync below and
# the syncs run concurrently on cold start, so the marketplace.json copy is
# staged exactly once here instead of racing rm+cp inside each sync.
stage_bundled_marketplace_metadata() {
    local source_marketplace="$SCRIPT_DIR/resources/plugins/openai-bundled/.agents/plugins/marketplace.json"
    local codex_home="${CODEX_HOME:-$HOME/.codex}"
    local marketplace_root="$codex_home/.tmp/bundled-marketplaces/openai-bundled"
    local marketplace_plugins_dir="$marketplace_root/.agents/plugins"
    local marketplace_target="$marketplace_plugins_dir/marketplace.json"
    local marketplace_temp=""

    [ -f "$source_marketplace" ] || return 0
    mkdir -p "$marketplace_plugins_dir" "$marketplace_root/plugins"
    marketplace_temp="$(mktemp "$marketplace_plugins_dir/.marketplace.json.tmp.XXXXXX")" || {
        echo "Bundled marketplace metadata sync failed; continuing with existing marketplace cache."
        return 0
    }
    if cp "$source_marketplace" "$marketplace_temp" && \
       chmod u+w "$marketplace_temp" && \
       mv -fT -- "$marketplace_temp" "$marketplace_target"; then
        return 0
    fi
    rm -f -- "$marketplace_temp"
    echo "Bundled marketplace metadata sync failed; continuing with existing marketplace cache."
}

sync_browser_use_bundled_plugin_cache() {
    local source_plugin="$SCRIPT_DIR/resources/plugins/openai-bundled/plugins/browser"
    local plugin_json=""
    local source_client=""
    local codex_home="${CODEX_HOME:-$HOME/.codex}"
    local plugin_name
    local plugin_dir_name
    local version
    local cache_root
    local cache_plugin
    local cache_client
    local cache_parent
    local tmp_plugin
    local marketplace_root
    local marketplace_plugins_dir
    local marketplace_plugin_link
    local needs_copy=1

    plugin_json="$source_plugin/.codex-plugin/plugin.json"
    source_client="$source_plugin/scripts/browser-client.mjs"

    [ -f "$plugin_json" ] || return 0
    [ -f "$source_client" ] || return 0

    plugin_name="$(bundled_plugin_name "$plugin_json" 2>/dev/null || basename "$source_plugin")"
    plugin_dir_name="$(basename "$source_plugin")"
    [ -n "$plugin_name" ] || return 0
    [ -n "$plugin_dir_name" ] || return 0

    marketplace_root="$codex_home/.tmp/bundled-marketplaces/openai-bundled"
    marketplace_plugins_dir="$marketplace_root/.agents/plugins"
    marketplace_plugin_link="$marketplace_root/plugins/$plugin_dir_name"
    mkdir -p "$marketplace_root/plugins"

    version="$(bundled_plugin_version "$plugin_json" 2>/dev/null || true)"
    [ -n "$version" ] || return 0

    cache_root="$codex_home/plugins/cache/openai-bundled/$plugin_name"
    cache_plugin="$cache_root/$version"
    cache_client="$cache_plugin/scripts/browser-client.mjs"

    if [ -f "$cache_client" ] && cmp -s "$source_client" "$cache_client"; then
        needs_copy=0
    fi

    if [ "$needs_copy" -eq 1 ]; then
        cache_parent="$(dirname "$cache_plugin")"
        tmp_plugin="$cache_parent/.browser-$version.tmp.$$"
        remove_tree_if_exists "$tmp_plugin"
        mkdir -p "$cache_parent"

        if cp -R "$source_plugin" "$tmp_plugin"; then
            make_tree_owner_writable "$tmp_plugin"
            find "$tmp_plugin" -type f -name '*:com.apple.*' -delete
            remove_tree_if_exists "$cache_plugin"
            mv "$tmp_plugin" "$cache_plugin"
            echo "Browser Use plugin cache synced from bundled resources: $cache_plugin"
        else
            remove_tree_if_exists "$tmp_plugin"
            echo "Browser Use plugin cache sync failed; continuing with existing cache."
            return 0
        fi
    fi

    replace_symlink "$version" "$cache_root/latest"

    mkdir -p "$marketplace_root/plugins"
    replace_symlink "$cache_root/latest" "$marketplace_plugin_link"
}

chrome_extension_host_arch() {
    case "$(uname -m)" in
        x86_64) echo "x64" ;;
        aarch64|arm64) echo "arm64" ;;
        *) return 1 ;;
    esac
}

write_chrome_native_host_manifests() {
    local host_path="$1"
    local plugin_dir="$2"

    python3 - "$host_path" "$HOME" "$plugin_dir" "$SCRIPT_DIR" <<'PY'
import json
import pathlib
import re
import sys

host_path = sys.argv[1]
home = pathlib.Path(sys.argv[2])
plugin_dir = pathlib.Path(sys.argv[3])
app_dir = pathlib.Path(sys.argv[4])
scripts_dir = plugin_dir / "scripts"

extension_id = None
host_name = None

extension_id_json = scripts_dir / "extension-id.json"
try:
    data = json.loads(extension_id_json.read_text(encoding="utf-8"))
    extension_id = data.get("extensionId")
    host_name = data.get("extensionHostName")
except OSError:
    pass

if extension_id is None or host_name is None:
    install_manifest = (scripts_dir / "installManifest.mjs").read_text(encoding="utf-8")
    extension_id_match = re.search(r'extensionId\s*:\s*"([a-p]{32})"', install_manifest)
    host_name_match = re.search(r'extensionHostName\s*:\s*"([A-Za-z0-9_.]+)"', install_manifest)
    if extension_id is None and extension_id_match is not None:
        extension_id = extension_id_match.group(1)
    if host_name is None and host_name_match is not None:
        host_name = host_name_match.group(1)

if not isinstance(extension_id, str) or re.fullmatch(r"[a-p]{32}", extension_id) is None:
    raise SystemExit("Invalid Chrome extension id in bundled plugin metadata")
if not isinstance(host_name, str) or re.fullmatch(r"[A-Za-z0-9_.]+", host_name) is None:
    raise SystemExit("Invalid Chrome native host name in bundled plugin metadata")

manifest_name = f"{host_name}.json"
manifest = {
    "name": host_name,
    "description": "Codex chrome native messaging host",
    "type": "stdio",
    "path": host_path,
    "allowed_origins": [f"chrome-extension://{extension_id}/"],
}
text = json.dumps(manifest, separators=(",", ":"))

manifest_locations = [
    ".config/google-chrome/NativeMessagingHosts",
    ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts",
    ".config/chromium/NativeMessagingHosts",
]

extra_locations = app_dir / ".codex-linux" / "chrome-native-host-manifest-paths"
try:
    for line in extra_locations.read_text(encoding="utf-8").splitlines():
        relative = line.strip()
        if (
            not relative
            or relative.startswith("#")
            or pathlib.PurePosixPath(relative).is_absolute()
            or ".." in pathlib.PurePosixPath(relative).parts
        ):
            continue
        if relative not in manifest_locations:
            manifest_locations.append(relative)
except OSError:
    pass

for relative in manifest_locations:
    directory = home / relative
    directory.mkdir(parents=True, exist_ok=True)
    path = directory / manifest_name
    try:
        if path.read_text(encoding="utf-8") == text:
            continue
    except OSError:
        pass
    path.write_text(text, encoding="utf-8")
PY
}

sync_chrome_bundled_plugin_cache() {
    local source_plugin="$SCRIPT_DIR/resources/plugins/openai-bundled/plugins/chrome"
    local plugin_json="$source_plugin/.codex-plugin/plugin.json"
    local codex_home="${CODEX_HOME:-$HOME/.codex}"
    local extension_arch
    local source_host
    local source_client
    local source_install_manifest
    local version
    local cache_root
    local cache_plugin
    local cache_host
    local cache_client
    local cache_install_manifest
    local cache_parent
    local tmp_plugin
    local marketplace_root
    local marketplace_plugins_dir
    local marketplace_plugin_link
    local host_path
    local needs_copy=1
    local cache_was_untrusted=0

    [ -f "$plugin_json" ] || return 0
    [ -d "$source_plugin" ] || return 0

    if ! extension_arch="$(chrome_extension_host_arch)"; then
        return 0
    fi

    source_host="$source_plugin/extension-host/linux/$extension_arch/extension-host"
    source_client="$source_plugin/scripts/browser-client.mjs"
    source_install_manifest="$source_plugin/scripts/installManifest.mjs"
    [ -x "$source_host" ] || return 0
    [ -f "$source_client" ] || return 0
    [ -f "$source_install_manifest" ] || return 0

    if path_has_unsafe_write \
        "$SCRIPT_DIR" \
        "$SCRIPT_DIR/resources" \
        "$SCRIPT_DIR/resources/plugins" \
        "$SCRIPT_DIR/resources/plugins/openai-bundled" \
        "$SCRIPT_DIR/resources/plugins/openai-bundled/plugins" \
        "$source_plugin" || tree_has_unsafe_write "$source_plugin"; then
        echo "Chrome bundled plugin source is not trusted; skipping cache and native-host manifest sync."
        return 0
    fi

    version="$(bundled_plugin_version "$plugin_json" 2>/dev/null || true)"
    [ -n "$version" ] || return 0

    cache_root="$codex_home/plugins/cache/openai-bundled/chrome"
    cache_plugin="$cache_root/$version"
    cache_host="$cache_plugin/extension-host/linux/$extension_arch/extension-host"
    cache_client="$cache_plugin/scripts/browser-client.mjs"
    cache_install_manifest="$cache_plugin/scripts/installManifest.mjs"

    if path_has_unsafe_write \
        "$codex_home" \
        "$codex_home/plugins" \
        "$codex_home/plugins/cache" \
        "$codex_home/plugins/cache/openai-bundled" \
        "$cache_root" || tree_has_unsafe_write "$cache_plugin"; then
        cache_was_untrusted=1
    fi

    if ! make_path_owner_trusted \
        "$codex_home" \
        "$codex_home/plugins" \
        "$codex_home/plugins/cache" \
        "$codex_home/plugins/cache/openai-bundled" \
        "$cache_root"; then
        echo "Chrome plugin cache ancestors could not be hardened; skipping cache and native-host manifest sync."
        return 0
    fi

    if [ "$cache_was_untrusted" -eq 0 ] && \
        [ -f "$cache_plugin/.codex-plugin/plugin.json" ] && \
        [ -f "$cache_host" ] && \
        [ -f "$cache_client" ] && \
        [ -f "$cache_install_manifest" ] && \
        cmp -s "$plugin_json" "$cache_plugin/.codex-plugin/plugin.json" && \
        cmp -s "$source_host" "$cache_host" && \
        cmp -s "$source_client" "$cache_client" && \
        cmp -s "$source_install_manifest" "$cache_install_manifest"; then
        needs_copy=0
    fi

    if [ "$needs_copy" -eq 0 ]; then
        local relative
        for relative in \
            scripts/check-extension-installed.js \
            scripts/check-native-host-manifest.js \
            scripts/chrome-is-running.js \
            scripts/extension-id.json \
            scripts/installed-browsers.js \
            scripts/open-chrome-window.js; do
            [ -f "$source_plugin/$relative" ] || continue
            if [ ! -f "$cache_plugin/$relative" ] || ! cmp -s "$source_plugin/$relative" "$cache_plugin/$relative"; then
                needs_copy=1
                break
            fi
        done
    fi

    if [ "$needs_copy" -eq 1 ]; then
        cache_parent="$(dirname "$cache_plugin")"
        tmp_plugin="$cache_parent/.chrome-$version.tmp.$$"
        remove_tree_if_exists "$tmp_plugin"
        mkdir -p "$cache_parent"
        if ! make_path_owner_trusted \
            "$codex_home" \
            "$codex_home/plugins" \
            "$codex_home/plugins/cache" \
            "$codex_home/plugins/cache/openai-bundled" \
            "$cache_root" \
            "$cache_parent"; then
            echo "Chrome plugin cache parent could not be hardened; skipping cache and native-host manifest sync."
            return 0
        fi

        if cp -R "$source_plugin" "$tmp_plugin"; then
            if ! make_tree_owner_trusted "$tmp_plugin"; then
                remove_tree_if_exists "$tmp_plugin"
                echo "Chrome plugin cache copy could not be hardened; continuing without a new cache."
                return 0
            fi
            find "$tmp_plugin" -type f -name '*:com.apple.*' -delete
            remove_tree_if_exists "$cache_plugin"
            mv "$tmp_plugin" "$cache_plugin"
            echo "Chrome plugin cache synced from bundled resources: $cache_plugin"
        else
            remove_tree_if_exists "$tmp_plugin"
            echo "Chrome plugin cache sync failed; continuing with existing cache."
            return 0
        fi
    fi

    if ! make_tree_owner_trusted "$cache_plugin"; then
        echo "Chrome plugin cache could not be hardened; skipping native-host manifest sync."
        return 0
    fi
    replace_symlink "$version" "$cache_root/latest"

    marketplace_root="$codex_home/.tmp/bundled-marketplaces/openai-bundled"
    marketplace_plugins_dir="$marketplace_root/.agents/plugins"
    marketplace_plugin_link="$marketplace_root/plugins/chrome"
    mkdir -p "$marketplace_plugins_dir" "$marketplace_root/plugins"
    replace_symlink "$cache_root/latest" "$marketplace_plugin_link"

    host_path="$cache_root/latest/extension-host/linux/$extension_arch/extension-host"
    [ -x "$host_path" ] || return 0
    write_chrome_native_host_manifests "$host_path" "$cache_root/latest" || \
        echo "Chrome native host manifest sync failed; continuing with existing browser manifests."
}

sync_computer_use_bundled_plugin_cache() {
    local source_plugin="$SCRIPT_DIR/resources/plugins/openai-bundled/plugins/computer-use"
    local plugin_json="$source_plugin/.codex-plugin/plugin.json"
    local source_backend="$source_plugin/bin/codex-computer-use-linux"
    local source_cosmic_helper="$source_plugin/bin/codex-computer-use-cosmic"
    local codex_home="${CODEX_HOME:-$HOME/.codex}"
    local version
    local cache_root
    local cache_plugin
    local cache_backend
    local cache_cosmic_helper
    local cache_parent
    local tmp_plugin
    local marketplace_root
    local marketplace_plugins_dir
    local marketplace_plugin_link
    local needs_copy=1

    [ -f "$plugin_json" ] || return 0
    [ -f "$source_backend" ] || return 0
    [ -f "$source_cosmic_helper" ] || return 0

    marketplace_root="$codex_home/.tmp/bundled-marketplaces/openai-bundled"
    marketplace_plugins_dir="$marketplace_root/.agents/plugins"
    marketplace_plugin_link="$marketplace_root/plugins/computer-use"
    mkdir -p "$marketplace_root/plugins"

    version="$(bundled_plugin_version "$plugin_json" 2>/dev/null || true)"
    [ -n "$version" ] || return 0

    cache_root="$codex_home/plugins/cache/openai-bundled/computer-use"
    cache_plugin="$cache_root/$version"
    cache_backend="$cache_plugin/bin/codex-computer-use-linux"
    cache_cosmic_helper="$cache_plugin/bin/codex-computer-use-cosmic"

    if [ -f "$cache_backend" ] && [ -f "$cache_cosmic_helper" ] && \
        cmp -s "$source_backend" "$cache_backend" && \
        cmp -s "$source_cosmic_helper" "$cache_cosmic_helper"; then
        needs_copy=0
    fi

    if [ "$needs_copy" -eq 1 ]; then
        cache_parent="$(dirname "$cache_plugin")"
        tmp_plugin="$cache_parent/.computer-use-$version.tmp.$$"
        remove_tree_if_exists "$tmp_plugin"
        mkdir -p "$cache_parent"

        if cp -R "$source_plugin" "$tmp_plugin"; then
            make_tree_owner_writable "$tmp_plugin"
            find "$tmp_plugin" -type f -name '*:com.apple.*' -delete
            remove_tree_if_exists "$cache_plugin"
            mv "$tmp_plugin" "$cache_plugin"
            echo "Computer Use plugin cache synced from bundled resources: $cache_plugin"
        else
            remove_tree_if_exists "$tmp_plugin"
            echo "Computer Use plugin cache sync failed; continuing with existing cache."
            return 0
        fi
    fi

    replace_symlink "$version" "$cache_root/latest"

    mkdir -p "$marketplace_root/plugins"
    replace_symlink "$cache_root/latest" "$marketplace_plugin_link"
}

sync_read_aloud_bundled_plugin_cache() {
    local source_plugin="$SCRIPT_DIR/resources/plugins/openai-bundled/plugins/read-aloud"
    local plugin_json="$source_plugin/.codex-plugin/plugin.json"
    local source_backend="$source_plugin/bin/codex-read-aloud-linux"
    local source_runner="$source_plugin/bin/kokoro-stdin"
    local source_python_runner="$source_plugin/bin/kokoro_stdin.py"
    local codex_home="${CODEX_HOME:-$HOME/.codex}"
    local version
    local cache_root
    local cache_plugin
    local cache_backend
    local cache_runner
    local cache_python_runner
    local cache_parent
    local tmp_plugin
    local marketplace_root
    local marketplace_plugins_dir
    local marketplace_plugin_link
    local needs_copy=1

    [ -f "$plugin_json" ] || return 0
    [ -x "$source_backend" ] || return 0
    [ -x "$source_runner" ] || return 0
    [ -f "$source_python_runner" ] || return 0

    marketplace_root="$codex_home/.tmp/bundled-marketplaces/openai-bundled"
    marketplace_plugins_dir="$marketplace_root/.agents/plugins"
    marketplace_plugin_link="$marketplace_root/plugins/read-aloud"
    mkdir -p "$marketplace_root/plugins"

    version="$(bundled_plugin_version "$plugin_json" 2>/dev/null || true)"
    [ -n "$version" ] || return 0

    cache_root="$codex_home/plugins/cache/openai-bundled/read-aloud"
    cache_plugin="$cache_root/$version"
    cache_backend="$cache_plugin/bin/codex-read-aloud-linux"
    cache_runner="$cache_plugin/bin/kokoro-stdin"
    cache_python_runner="$cache_plugin/bin/kokoro_stdin.py"

    if [ -f "$cache_backend" ] && [ -f "$cache_runner" ] && [ -f "$cache_python_runner" ] && \
        cmp -s "$source_backend" "$cache_backend" && \
        cmp -s "$source_runner" "$cache_runner" && \
        cmp -s "$source_python_runner" "$cache_python_runner"; then
        needs_copy=0
    fi

    if [ "$needs_copy" -eq 1 ]; then
        cache_parent="$(dirname "$cache_plugin")"
        tmp_plugin="$cache_parent/.read-aloud-$version.tmp.$$"
        remove_tree_if_exists "$tmp_plugin"
        mkdir -p "$cache_parent"

        if cp -R "$source_plugin" "$tmp_plugin"; then
            make_tree_owner_writable "$tmp_plugin"
            find "$tmp_plugin" -type f -name '*:com.apple.*' -delete
            remove_tree_if_exists "$cache_plugin"
            mv "$tmp_plugin" "$cache_plugin"
            echo "Read Aloud plugin cache synced from bundled resources: $cache_plugin"
        else
            remove_tree_if_exists "$tmp_plugin"
            echo "Read Aloud plugin cache sync failed; continuing with existing cache."
            return 0
        fi
    fi

    replace_symlink "$version" "$cache_root/latest"

    mkdir -p "$marketplace_root/plugins"
    replace_symlink "$cache_root/latest" "$marketplace_plugin_link"
}

sync_extra_bundled_plugin_cache() {
    local plugins_root="$SCRIPT_DIR/resources/plugins/openai-bundled/plugins"
    local codex_home="${CODEX_HOME:-$HOME/.codex}"
    local source_plugin
    local plugin_json
    local plugin_name
    local plugin_dir_name
    local version
    local cache_root
    local cache_plugin
    local cache_parent
    local tmp_plugin
    local backup_plugin
    local marketplace_root
    local marketplace_plugins_dir
    local marketplace_plugin_link

    [ -d "$plugins_root" ] || return 0

    for source_plugin in "$plugins_root"/*; do
        [ -d "$source_plugin" ] || continue
        plugin_dir_name="$(basename "$source_plugin")"
        case "$plugin_dir_name" in
            browser|chrome|computer-use|read-aloud) continue ;;
        esac

        plugin_json="$source_plugin/.codex-plugin/plugin.json"
        [ -f "$plugin_json" ] || continue

        plugin_name="$(bundled_plugin_name "$plugin_json" 2>/dev/null || true)"
        version="$(bundled_plugin_version "$plugin_json" 2>/dev/null || true)"
        [ -n "$plugin_name" ] || continue
        [ -n "$version" ] || continue

        cache_root="$codex_home/plugins/cache/openai-bundled/$plugin_name"
        cache_plugin="$cache_root/$version"
        cache_parent="$(dirname "$cache_plugin")"
        tmp_plugin="$cache_parent/.$plugin_name-$version.tmp.$$"
        backup_plugin="$cache_parent/.$plugin_name-$version.backup.$$"
        if ! remove_tree_if_exists "$tmp_plugin" || \
           ! remove_tree_if_exists "$backup_plugin" || \
           ! mkdir -p "$cache_parent"; then
            echo "Extra bundled plugin cache preparation failed for $plugin_name; continuing with existing cache."
            continue
        fi

        if cp -R "$source_plugin" "$tmp_plugin"; then
            make_tree_owner_writable "$tmp_plugin"
            if ! find "$tmp_plugin" -type f -name '*:com.apple.*' -delete; then
                remove_tree_if_exists "$tmp_plugin" || true
                echo "Extra bundled plugin cache cleanup failed for $plugin_name; continuing with existing cache."
                continue
            fi
            if [ -e "$cache_plugin" ] || [ -L "$cache_plugin" ]; then
                if ! mv -T -- "$cache_plugin" "$backup_plugin"; then
                    remove_tree_if_exists "$tmp_plugin" || true
                    echo "Extra bundled plugin cache sync failed for $plugin_name; existing cache was preserved."
                    continue
                fi
            else
                backup_plugin=""
            fi
            if ! mv -T -- "$tmp_plugin" "$cache_plugin"; then
                remove_tree_if_exists "$tmp_plugin" || true
                if [ -n "$backup_plugin" ]; then
                    if mv -T -- "$backup_plugin" "$cache_plugin"; then
                        echo "Extra bundled plugin cache sync failed for $plugin_name; previous cache was restored."
                    else
                        echo "Extra bundled plugin cache sync failed for $plugin_name and the previous cache could not be restored."
                    fi
                else
                    echo "Extra bundled plugin cache sync failed for $plugin_name; continuing without a new cache."
                fi
                continue
            fi
            if [ -n "$backup_plugin" ]; then
                remove_tree_if_exists "$backup_plugin" || \
                    echo "Extra bundled plugin cache backup cleanup failed for $plugin_name: $backup_plugin"
            fi
            echo "Extra bundled plugin cache synced from bundled resources: $cache_plugin"
        else
            remove_tree_if_exists "$tmp_plugin" || true
            echo "Extra bundled plugin cache sync failed for $plugin_name; continuing with existing cache."
            continue
        fi

        if ! replace_symlink "$version" "$cache_root/latest"; then
            echo "Extra bundled plugin latest link failed for $plugin_name; continuing with the staged cache."
            continue
        fi

        marketplace_root="$codex_home/.tmp/bundled-marketplaces/openai-bundled"
        marketplace_plugins_dir="$marketplace_root/.agents/plugins"
        marketplace_plugin_link="$marketplace_root/plugins/$plugin_dir_name"
        if ! mkdir -p "$marketplace_plugins_dir" "$marketplace_root/plugins" || \
           ! replace_symlink "$cache_root/latest" "$marketplace_plugin_link"; then
            echo "Extra bundled plugin marketplace link failed for $plugin_name; continuing with the staged cache."
        fi
    done
}

resolve_browser_use_runtime_env() {
    if [ -z "${CODEX_ELECTRON_RESOURCES_PATH:-}" ]; then
        export CODEX_ELECTRON_RESOURCES_PATH="$SCRIPT_DIR/resources"
    fi

    if [ -z "${CODEX_BROWSER_USE_NODE_PATH:-}" ]; then
        if [ -x "$MANAGED_NODE_BIN_DIR/node" ]; then
            export CODEX_BROWSER_USE_NODE_PATH="$MANAGED_NODE_BIN_DIR/node"
        elif [ -x "$SCRIPT_DIR/resources/node" ]; then
            export CODEX_BROWSER_USE_NODE_PATH="$SCRIPT_DIR/resources/node"
        elif command -v node >/dev/null 2>&1; then
            CODEX_BROWSER_USE_NODE_PATH="$(command -v node)"
            export CODEX_BROWSER_USE_NODE_PATH
        fi
    fi

    if [ -z "${NODE_REPL_NODE_PATH:-}" ] && [ -n "${CODEX_BROWSER_USE_NODE_PATH:-}" ]; then
        export NODE_REPL_NODE_PATH="$CODEX_BROWSER_USE_NODE_PATH"
    fi

    if [ -z "${CODEX_NODE_REPL_PATH:-}" ]; then
        codex_runtime_node_repl="${XDG_CACHE_HOME:-$HOME/.cache}/codex-runtimes/codex-primary-runtime/dependencies/bin/node_repl"
        if [ -x "$SCRIPT_DIR/resources/node_repl" ]; then
            export CODEX_NODE_REPL_PATH="$SCRIPT_DIR/resources/node_repl"
        elif command -v node_repl >/dev/null 2>&1; then
            CODEX_NODE_REPL_PATH="$(command -v node_repl)"
            export CODEX_NODE_REPL_PATH
        elif [ -x "$codex_runtime_node_repl" ]; then
            export CODEX_NODE_REPL_PATH="$codex_runtime_node_repl"
        fi
    fi

    if [ -z "${CODEX_NODE_REPL_PATH:-}" ]; then
        echo "Browser Use node_repl runtime not found; in-app browser automation may be unavailable."
    fi
}

run_cold_start_hooks() {
    [ -d "$COLD_START_HOOK_DIR" ] || return 0
    local hook
    for hook in "$COLD_START_HOOK_DIR"/*; do
        [ -f "$hook" ] || continue
        [ -x "$hook" ] || continue
        echo "Starting cold-start hook: $hook"
        (
            CODEX_HOME="$CODEX_HOME" \
            CODEX_LINUX_APP_DIR="$SCRIPT_DIR" \
            CODEX_LINUX_APP_STATE_DIR="$APP_STATE_DIR" \
            CODEX_LINUX_FEATURES_DIR="$CODEX_LINUX_FEATURES_DIR" \
            CODEX_LINUX_LAUNCHER_LOG="$LOG_FILE" \
            CODEX_LINUX_LAUNCHER_CMD="${CODEX_LINUX_LAUNCHER_CMD:-$SCRIPT_DIR/start.sh}" \
            CODEX_LINUX_FEATURE_HOOK_PHASE="cold-start" \
            CODEX_UPDATE_MANAGER_PATH="${CODEX_UPDATE_MANAGER_PATH:-}" \
            codex_run_host_command "$hook" "$SCRIPT_DIR" "$APP_STATE_DIR" "$LOG_DIR"
        ) >>"$LOG_FILE" 2>&1 &
    done
}

run_feature_after_exit_hooks() {
    [ -n "${FEATURE_AFTER_EXIT_HOOK_DIR:-}" ] || return 0
    [ -d "$FEATURE_AFTER_EXIT_HOOK_DIR" ] || return 0

    local electron_status="${1:-0}"
    local hook hook_status
    for hook in "$FEATURE_AFTER_EXIT_HOOK_DIR"/*; do
        [ -f "$hook" ] || continue
        [ -x "$hook" ] || continue
        echo "Running Linux feature after-exit hook: $hook"
        set +e
        CODEX_HOME="$CODEX_HOME" \
        CODEX_LINUX_APP_DIR="$SCRIPT_DIR" \
        CODEX_LINUX_APP_STATE_DIR="$APP_STATE_DIR" \
        CODEX_LINUX_FEATURES_DIR="$CODEX_LINUX_FEATURES_DIR" \
        CODEX_LINUX_LAUNCHER_LOG="$LOG_FILE" \
        CODEX_LINUX_LAUNCHER_CMD="${CODEX_LINUX_LAUNCHER_CMD:-$SCRIPT_DIR/start.sh}" \
        CODEX_LINUX_FEATURE_HOOK_PHASE="after-exit" \
        CODEX_LINUX_ELECTRON_EXIT_STATUS="$electron_status" \
        CODEX_UPDATE_MANAGER_PATH="${CODEX_UPDATE_MANAGER_PATH:-}" \
            codex_run_host_command "$hook" "$SCRIPT_DIR" "$APP_STATE_DIR" "$LOG_DIR" "$electron_status"
        hook_status=$?
        set -e
        if [ "$hook_status" -ne 0 ]; then
            echo "Linux feature after-exit hook failed with status $hook_status: $hook"
        fi
    done
}

run_cli_preflight() {
    local allow_install_missing="${1:-0}"
    local require_success="${2:-0}"
    if ! has_update_manager; then
        if [ "$allow_install_missing" = "1" ] || [ "$require_success" = "1" ]; then
            return 1
        fi
        return 0
    fi

    local -a preflight_args=(
        cli-preflight
        --print-path
    )
    local preflight_cli_path="${CODEX_CLI_SOURCE_PATH:-${CODEX_CLI_PATH:-}}"
    if [ -n "$preflight_cli_path" ]; then
        preflight_args+=(--cli-path "$preflight_cli_path")
    fi
    if [ "$allow_install_missing" = "1" ]; then
        preflight_args+=(--allow-install-missing)
    fi

    local refreshed_path=""
    if ! refreshed_path="$(run_update_manager "${preflight_args[@]}")"; then
        if [ "$allow_install_missing" = "1" ] || [ "$require_success" = "1" ]; then
            return 1
        fi
        notify_error "Codex CLI prelaunch check failed. Continuing with the current CLI state. Check the launcher and updater logs if ChatGPT Desktop misbehaves."
        return 0
    fi

    if [ -n "$refreshed_path" ]; then
        CODEX_CLI_PATH="$refreshed_path"
        export CODEX_CLI_PATH
    fi
}

run_cli_preflight_background() {
    if ! has_update_manager; then
        return 0
    fi

    (
        local -a args=(cli-preflight --print-path)
        local preflight_cli_path="${CODEX_CLI_SOURCE_PATH:-${CODEX_CLI_PATH:-}}"
        if [ -n "$preflight_cli_path" ]; then
            args+=(--cli-path "$preflight_cli_path")
        fi
        if ! run_update_manager "${args[@]}" >/dev/null 2>&1; then
            echo "Codex CLI background preflight failed. Continuing with the current CLI."
        fi
    ) &
}

run_cli_launch_path_helper() {
    python3 "$SCRIPT_DIR/.codex-linux/cli-launch-path.py" "$1"
}

verify_cli_launch_path() {
    local selected_path="${CODEX_CLI_PATH:-}"
    local verified_path=""

    [ -n "$selected_path" ] || return 1
    if ! verified_path="$(run_cli_launch_path_helper "$selected_path")"; then
        return 1
    fi
    [ -n "$verified_path" ] || return 1

    CODEX_CLI_SOURCE_PATH="$selected_path"
    CODEX_CLI_PATH="$verified_path"
    export CODEX_CLI_SOURCE_PATH CODEX_CLI_PATH
}

is_interactive_terminal() {
    [ "$LAUNCHER_INTERACTIVE_TERMINAL" -eq 1 ] && [ -r /dev/tty ] && [ -w /dev/tty ]
}

run_gui_cli_prompt() {
    if ! has_update_manager; then
        return 1
    fi

    local refreshed_path=""
    local -a args=(prompt-install-cli --print-path)
    if [ -n "${CODEX_CLI_PATH:-}" ]; then
        args+=(--cli-path "$CODEX_CLI_PATH")
    fi
    if ! refreshed_path="$(run_update_manager "${args[@]}")"; then
        return 1
    fi

    if [ -n "$refreshed_path" ]; then
        CODEX_CLI_PATH="$refreshed_path"
        export CODEX_CLI_PATH
    fi

    return 0
}

prompt_install_missing_cli() {
    if ! is_interactive_terminal; then
        return 1
    fi

    if ! has_update_manager; then
        return 1
    fi

    local reply=""
    # stdout is redirected into the launcher log, so prompt via the terminal.
    printf 'Codex CLI is not installed. Install it now? [Y/n] ' >/dev/tty
    if ! read -r reply </dev/tty; then
        return 1
    fi

    case "$reply" in
        ""|y|Y|yes|YES|Yes)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

resolve_notification_icon() {
    local candidate
    for candidate in \
        "$APP_NOTIFICATION_ICON_BUNDLE" \
        "$APP_NOTIFICATION_ICON_SYSTEM" \
        "$APP_NOTIFICATION_ICON_REPO"
    do
        if [ -f "$candidate" ]; then
            echo "$candidate"
            return 0
        fi
    done

    echo "$APP_NOTIFICATION_ICON_NAME"
}

cached_codex_cli_path() {
    local state_file="${XDG_STATE_HOME:-$HOME/.local/state}/codex-update-manager/state.json"
    [ -r "$state_file" ] || return 1
    command -v python3 >/dev/null 2>&1 || return 1

    python3 - "$state_file" <<'PY'
import json
import sys

try:
    with open(sys.argv[1], "r", encoding="utf-8") as handle:
        value = json.load(handle).get("cli_path")
except Exception:
    sys.exit(1)
if isinstance(value, str) and value:
    print(value)
PY
}

find_fnm_codex_cli() {
    local candidate root

    if [ -n "${FNM_MULTISHELL_PATH:-}" ]; then
        candidate="$FNM_MULTISHELL_PATH/bin/codex"
        if [ -x "$candidate" ]; then
            echo "$candidate"
            return 0
        fi
    fi

    for root in \
        "${FNM_DIR:-}" \
        "${XDG_DATA_HOME:-$HOME/.local/share}/fnm" \
        "$HOME/.fnm"
    do
        [ -n "$root" ] || continue
        candidate="$root/aliases/default/bin/codex"
        if [ -x "$candidate" ]; then
            echo "$candidate"
            return 0
        fi
    done

    return 1
}

find_codex_cli() {
    if command -v codex >/dev/null 2>&1; then
        command -v codex
        return 0
    fi

    if [ -n "${HOMEBREW_PREFIX:-}" ]; then
        local homebrew_candidate="$HOMEBREW_PREFIX/bin/codex"
        if [ -x "$homebrew_candidate" ]; then
            echo "$homebrew_candidate"
            return 0
        fi
    fi

    local nvm_dir="${NVM_DIR:-}"
    if [ -z "$nvm_dir" ]; then
        if [ -s "${XDG_CONFIG_HOME:-$HOME/.config}/nvm/nvm.sh" ]; then
            nvm_dir="${XDG_CONFIG_HOME:-$HOME/.config}/nvm"
        else
            nvm_dir="$HOME/.nvm"
        fi
    fi
    if [ -s "$nvm_dir/nvm.sh" ]; then
        export NVM_DIR="$nvm_dir"
        # shellcheck disable=SC1090
        . "$NVM_DIR/nvm.sh" >/dev/null 2>&1 || true
        if command -v codex >/dev/null 2>&1; then
            command -v codex
            return 0
        fi
    fi

    local candidate
    for candidate in \
        "$(cached_codex_cli_path || true)" \
        "$HOME/.bun/bin/codex" \
        "$HOME/.codex-cli-npm/bin/codex" \
        "$HOME/.npm-global/bin/codex" \
        "${XDG_CONFIG_HOME:-$HOME/.config}/nvm/versions/node/current/bin/codex" \
        "${XDG_CONFIG_HOME:-$HOME/.config}/nvm/versions/node"/*/bin/codex \
        "$HOME/.nvm/versions/node/current/bin/codex" \
        "$HOME/.nvm/versions/node"/*/bin/codex \
        "$(find_fnm_codex_cli || true)" \
        "$HOME/.local/share/pnpm/codex" \
        "$HOME/.linuxbrew/bin/codex" \
        "/home/linuxbrew/.linuxbrew/bin/codex" \
        "$HOME/.local/bin/codex" \
        "/usr/local/bin/codex" \
        "/usr/bin/codex"
    do
        if [ -x "$candidate" ]; then
            echo "$candidate"
            return 0
        fi
    done

    return 1
}

pid_parent_matches() {
    local pid="$1"
    local expected_parent="$2"
    local stat_line
    local rest

    [ -r "/proc/$pid/stat" ] || return 1
    stat_line=$(< "/proc/$pid/stat") || return 1
    rest=${stat_line##*) }
    set -- $rest
    # $1 = state, $2 = ppid
    [ "${2:-}" = "$expected_parent" ]
}

codex_cli_version_probe() {
    local output_file="$1"
    local stderr_file="$2"
    shift 2
    local marker="${output_file}.timed-out"
    local probe_pid
    local watchdog_pid
    # Under command substitution, $$ stays the top-level launcher PID.
    local self_pid="${BASHPID:-$$}"
    local status=0

    rm -f "$marker"
    ( trap - EXIT
      { exec 9>&-; } 2>/dev/null || true
      codex_exec_host_command "$@" >"$output_file" 2>"$stderr_file"
    ) &
    probe_pid="$!"
    # The watchdog (and its sleep child, which outlives the kill below) must
    # not inherit the caller's stdout: codex_cli_version runs under command
    # substitution, and an inherited pipe fd would block the caller for the
    # full watchdog second even when the probe answers immediately.
    ( trap - EXIT
      { exec 9>&-; } 2>/dev/null || true
      sleep 1
      if pid_parent_matches "$probe_pid" "$self_pid"; then
          : > "$marker"
          kill "$probe_pid" 2>/dev/null || true
          sleep 0.05
          if pid_parent_matches "$probe_pid" "$self_pid"; then
              kill -9 "$probe_pid" 2>/dev/null || true
          fi
      fi
    ) >/dev/null 2>&1 &
    watchdog_pid="$!"

    if wait "$probe_pid"; then
        status=0
    else
        status=$?
    fi
    kill "$watchdog_pid" 2>/dev/null || true
    wait "$watchdog_pid" 2>/dev/null || true

    if [ -e "$marker" ]; then
        rm -f "$marker"
        return 124
    fi
    rm -f "$marker"
    return "$status"
}

codex_cli_version() {
    local cli_path="$1"
    local probe_file
    local output
    local version
    local version_arg
    local probe_status=0

    [ -n "$cli_path" ] || return 1
    [ -x "$cli_path" ] || return 1
    probe_file="$(mktemp "${TMPDIR:-/tmp}/codex-cli-version.XXXXXX" 2>/dev/null)" || return 1

    for version_arg in --version version; do
        : > "$probe_file"
        if codex_cli_version_probe "$probe_file" /dev/null "$cli_path" "$version_arg"; then
            output="$(sed -n '1,3p' "$probe_file" 2>/dev/null || true)"
            version="$(printf '%s\n' "$output" | sed -nE '
s/^v?([0-9]+([.][0-9]+)+(-[0-9A-Za-z.-]+)?([+][0-9A-Za-z.-]+)?)([^0-9A-Za-z._+-].*)?$/\1/p
s/.*[^0-9A-Za-z._+-]v?([0-9]+([.][0-9]+)+(-[0-9A-Za-z.-]+)?([+][0-9A-Za-z.-]+)?)([^0-9A-Za-z._+-].*)?$/\1/p
' | head -n 1)"
            if [ -n "$version" ]; then
                rm -f "$probe_file" "${probe_file}.timed-out"
                echo "$version"
                return 0
            fi
        else
            probe_status=$?
            if [ "$probe_status" -eq 124 ]; then
                rm -f "$probe_file" "${probe_file}.timed-out"
                return 1
            fi
        fi
    done

    rm -f "$probe_file" "${probe_file}.timed-out"
    return 1
}

codex_cli_missing_optional_dependency() {
    local cli_path="$1"
    local output_file
    local stderr_file
    local probe_status

    [ -n "$cli_path" ] || return 1
    [ -x "$cli_path" ] || return 1
    output_file="$(mktemp "${TMPDIR:-/tmp}/codex-cli-output.XXXXXX" 2>/dev/null)" || return 1
    stderr_file="$(mktemp "${TMPDIR:-/tmp}/codex-cli-error.XXXXXX" 2>/dev/null)" || {
        rm -f "$output_file"
        return 1
    }

    if codex_cli_version_probe "$output_file" "$stderr_file" "$cli_path" --version; then
        probe_status=0
    else
        probe_status=$?
    fi
    if [ "$probe_status" -ne 0 ] && [ "$probe_status" -ne 124 ] && \
        grep -Eq 'Missing optional dependency[[:space:]]*@openai/codex-linux-(x64|arm64)([[:space:].,:;)]|$)' "$output_file" "$stderr_file"; then
        rm -f "$output_file" "$stderr_file" "${output_file}.timed-out"
        return 0
    fi

    rm -f "$output_file" "$stderr_file" "${output_file}.timed-out"
    return 1
}

log_codex_cli_path() {
    local version

    if [ -z "${CODEX_CLI_PATH:-}" ]; then
        echo "Using CODEX_CLI_PATH=warm-start-skip"
        return 0
    fi

    version="$(codex_cli_version "$CODEX_CLI_PATH" || true)"
    if [ -n "$version" ]; then
        echo "Using CODEX_CLI_PATH=$CODEX_CLI_PATH (version $version)"
    else
        echo "Using CODEX_CLI_PATH=$CODEX_CLI_PATH (version unknown; set CODEX_CLI_PATH=/path/to/codex to pin a known CLI)"
    fi
}

notify_error() {
    local message="$1"
    local icon
    icon="$(resolve_notification_icon)"
    echo "$message"
    if command -v notify-send >/dev/null 2>&1; then
        codex_run_host_command notify-send \
            -a "$CODEX_LINUX_APP_DISPLAY_NAME" \
            -i "$icon" \
            -h "string:desktop-entry:$CODEX_LINUX_APP_ID" \
            "$CODEX_LINUX_APP_DISPLAY_NAME" \
            "$message"
    fi
}

canonical_path() {
    readlink -f "$1" 2>/dev/null || echo "$1"
}

resolve_update_manager_path() {
    if [ -n "${CODEX_UPDATE_MANAGER_PATH:-}" ] && [ -x "$CODEX_UPDATE_MANAGER_PATH" ]; then
        return 0
    fi

    local candidate
    for candidate in \
        "$SCRIPT_DIR/../target/release/codex-update-manager" \
        "$SCRIPT_DIR/codex-update-manager" \
        "/usr/bin/codex-update-manager" \
        "/usr/local/bin/codex-update-manager"
    do
        if [ -x "$candidate" ]; then
            CODEX_UPDATE_MANAGER_PATH="$(canonical_path "$candidate")"
            export CODEX_UPDATE_MANAGER_PATH
            return 0
        fi
    done

    if command -v codex-update-manager >/dev/null 2>&1; then
        CODEX_UPDATE_MANAGER_PATH="$(command -v codex-update-manager)"
        export CODEX_UPDATE_MANAGER_PATH
        return 0
    fi

    return 1
}

has_update_manager() {
    resolve_update_manager_path >/dev/null 2>&1
}

export_feature_runtime_env() {
    CODEX_LINUX_APP_DIR="$SCRIPT_DIR"
    CODEX_LINUX_APP_STATE_DIR="$APP_STATE_DIR"
    CODEX_LINUX_LAUNCHER_CMD="$SCRIPT_DIR/start.sh"
    export CODEX_LINUX_APP_DIR CODEX_LINUX_APP_STATE_DIR CODEX_LINUX_LAUNCHER_CMD
    resolve_update_manager_path >/dev/null 2>&1 || true
}

run_update_manager() {
    resolve_update_manager_path || return 127
    codex_run_host_command "$CODEX_UPDATE_MANAGER_PATH" "$@"
}

pid_is_current_user() {
    local pid="$1"
    local key
    local uid

    [ -r "/proc/$pid/status" ] || return 1
    while IFS=$' \t' read -r key uid _; do
        if [ "$key" = "Uid:" ]; then
            [ "$uid" = "$EUID" ]
            return $?
        fi
    done < "/proc/$pid/status"
    return 1
}

pid_is_electron_helper() {
    local pid="$1"
    local cmdline

    cmdline="$(pid_cmdline "$pid")"
    [[ "$cmdline" == --type=* || "$cmdline" == *" --type="* ]]
}

pid_cmdline() {
    local pid="$1"
    local part
    local cmdline=""

    [ -r "/proc/$pid/cmdline" ] || return 0
    while IFS= read -r -d '' part; do
        if [ -n "$cmdline" ]; then
            cmdline="$cmdline $part"
        else
            cmdline="$part"
        fi
        part=""
    done < "/proc/$pid/cmdline" 2>/dev/null || true
    if [ -n "${part:-}" ]; then
        if [ -n "$cmdline" ]; then
            cmdline="$cmdline $part"
        else
            cmdline="$part"
        fi
    fi
    printf '%s\n' "$cmdline"
}

pid_cmdline_arg0() {
    local pid="$1"
    local arg0=""

    [ -r "/proc/$pid/cmdline" ] || return 0
    IFS= read -r -d '' arg0 < "/proc/$pid/cmdline" 2>/dev/null || true
    printf '%s\n' "$arg0"
}

pid_cmdline_arg0_path() {
    local pid="$1"
    local arg0

    arg0="$(pid_cmdline_arg0 "$pid")"
    printf '%s\n' "$arg0"
}

pid_arg0_matches_path() {
    local actual="$1"
    local expected="$2"

    case "$actual" in
        "$expected"|"$expected "*) return 0 ;;
        *) return 1 ;;
    esac
}

pid_environ_lines() {
    local pid="$1"
    [ -r "/proc/$pid/environ" ] || return 0
    tr '\0' '\n' 2>/dev/null < "/proc/$pid/environ" || true
}

pid_environ_value() {
    local pid="$1"
    local key="$2"
    local entry

    [ -r "/proc/$pid/environ" ] || return 1
    while IFS= read -r -d '' entry; do
        case "$entry" in
            "$key="*) printf '%s\n' "${entry#*=}"; return 0 ;;
        esac
    done < "/proc/$pid/environ" 2>/dev/null
    return 1
}

pid_start_time() {
    local pid="$1"
    local stat_line
    local rest

    [ -r "/proc/$pid/stat" ] || return 1
    stat_line=$(< "/proc/$pid/stat") || return 1
    rest=${stat_line##*) }
    set -- $rest
    # /proc/<pid>/stat field 22 is token 20 after stripping pid and comm.
    [ -n "${20:-}" ] || return 1
    printf '%s\n' "${20}"
}

pid_matches_executable() {
    local pid="$1"
    local expected="$2"
    local actual

    [[ "$pid" =~ ^[0-9]+$ ]] || return 1
    [ -d "/proc/$pid" ] || return 1
    actual="$(pid_cmdline_arg0_path "$pid")"
    pid_arg0_matches_path "$actual" "$expected" || return 1
    pid_is_current_user "$pid" || return 1
    ! pid_is_electron_helper "$pid"
}

find_running_app_pid() {
    local pid

    if [ -f "$APP_PID_FILE" ]; then
        pid="$(cat "$APP_PID_FILE" 2>/dev/null || true)"
        if pid_matches_executable "$pid" "$SCRIPT_DIR/electron"; then
            echo "$pid"
            return 0
        fi
    fi

    return 1
}

pid_in_same_launch_instance() {
    # The /proc scan matches by electron binary path, which side-by-side
    # multi-launch instances share with the default app. Match instance
    # identity via the process environment so the default app never adopts
    # a side-by-side instance and vice versa. Substring checks on the
    # newline-joined environ avoid grep -q pipelines, which can fail under
    # pipefail when grep exits before the writer finishes.
    local pid="$1"
    local environ="/proc/$pid/environ"
    local env_lines

    env_lines=$'\n'"$(pid_environ_lines "$pid")"$'\n'
    if [ "$MULTI_LAUNCH_ACTIVE" -eq 1 ]; then
        [[ "$env_lines" == *$'\n'"CODEX_LINUX_INSTANCE_ID=$CODEX_LINUX_INSTANCE_ID"$'\n'* ]]
    else
        [[ "$env_lines" != *$'\n'"CODEX_LINUX_MULTI_LAUNCH=1"$'\n'* ]]
    fi
}

pid_matches_app_identity() {
    local pid="$1"
    local env_lines
    local cmdline

    [[ "$pid" =~ ^[0-9]+$ ]] || return 1
    [ -d "/proc/$pid" ] || return 1
    pid_is_current_user "$pid" || return 1
    env_lines=$'\n'"$(pid_environ_lines "$pid")"$'\n'
    if [[ "$env_lines" == *$'\n'"CODEX_LINUX_APP_ID=$CODEX_LINUX_APP_ID"$'\n'* ]] ||
       [[ "$env_lines" == *$'\n'"CODEX_APP_ID=$CODEX_LINUX_APP_ID"$'\n'* ]]; then
        return 0
    fi
    cmdline="$(pid_cmdline "$pid")"
    [[ "$cmdline" == *"--app-id=$CODEX_LINUX_APP_ID"* ]]
}

pid_is_foreign_codex_electron() {
    local pid="$1"
    local actual

    [[ "$pid" =~ ^[0-9]+$ ]] || return 1
    [ -d "/proc/$pid" ] || return 1
    actual="$(pid_cmdline_arg0_path "$pid")"
    [ -n "$actual" ] || return 1
    ! pid_arg0_matches_path "$actual" "$SCRIPT_DIR/electron" || return 1
    case "${actual##*/}" in
        electron|electron\ *) ;;
        *) return 1 ;;
    esac
    pid_is_current_user "$pid" || return 1
    ! pid_is_electron_helper "$pid" || return 1
    pid_matches_app_identity "$pid"
}

discover_running_app_pid() {
    local pid
    local proc_cmdline

    for proc_cmdline in /proc/[0-9]*/cmdline; do
        [ -r "$proc_cmdline" ] || continue
        pid="${proc_cmdline#/proc/}"
        pid="${pid%/cmdline}"
        if pid_matches_executable "$pid" "$SCRIPT_DIR/electron" && pid_in_same_launch_instance "$pid"; then
            echo "$pid"
            return 0
        fi
    done

    return 1
}

discover_foreign_app_pid() {
    local pid
    local proc_cmdline

    for proc_cmdline in /proc/[0-9]*/cmdline; do
        [ -r "$proc_cmdline" ] || continue
        pid="${proc_cmdline#/proc/}"
        pid="${pid%/cmdline}"
        if pid_is_foreign_codex_electron "$pid"; then
            echo "$pid"
            return 0
        fi
    done

    return 1
}

pid_install_dir() {
    local pid="$1"
    local actual

    actual="$(pid_cmdline_arg0_path "$pid")"
    [ -n "$actual" ] || return 1
    dirname "$actual"
}

pid_summary() {
    local pid="$1"
    local exe
    local cmdline

    exe="$(pid_cmdline_arg0_path "$pid")"
    cmdline="$(pid_cmdline "$pid")"
    printf 'pid=%s exe=%s cmd=%s' "$pid" "${exe:-unknown}" "${cmdline:-unknown}"
}

detect_cross_install_conflict() {
    local pid
    local install_dir
    local detail

    [ "$MULTI_LAUNCH_ACTIVE" -eq 0 ] || return 0
    if ! pid="$(discover_foreign_app_pid)"; then
        return 0
    fi

    install_dir="$(pid_install_dir "$pid" 2>/dev/null || echo "unknown")"
    detail="Another $CODEX_LINUX_APP_DISPLAY_NAME is already running from $install_dir (pid $pid), while this launcher is $SCRIPT_DIR. Both use app id '$CODEX_LINUX_APP_ID', webview port $CODEX_LINUX_WEBVIEW_PORT, config $APP_CONFIG_DIR, and state $APP_STATE_DIR. Close the other app, or launch one build with a side-by-side CODEX_APP_ID/CODEX_LINUX_APP_ID or --new-instance."
    echo "Foreign ChatGPT Desktop process: $(pid_summary "$pid")"
    notify_error "$detail"
    exit 1
}

runtime_recovery_scan_needed() {
    [ -f "$APP_PID_FILE" ] && return 0
    [ -e "$LAUNCH_ACTION_SOCKET" ] && return 0
    [ -f "$WEBVIEW_PID_FILE" ] && return 0
    return 1
}

set_detected_running_app() {
    RUNNING_APP_PID="$1"
    echo "$RUNNING_APP_PID" > "$APP_PID_FILE"

    if ! linux_setting_enabled "codex-linux-warm-start-enabled" 1; then
        WARM_START=0
        echo "Warm-start handoff disabled by $APP_SETTINGS_FILE"
        echo "Detected running ChatGPT Desktop pid=$RUNNING_APP_PID; preserving liveness marker for second-instance handoff"
        return 0
    fi

    WARM_START=1
    echo "Detected running ChatGPT Desktop pid=$RUNNING_APP_PID; using warm-start handoff"
}

running_app_is_active() {
    [ -n "${RUNNING_APP_PID:-}" ] && pid_matches_executable "$RUNNING_APP_PID" "$SCRIPT_DIR/electron"
}

using_second_instance_handoff() {
    [ "$WARM_START" -eq 0 ] && running_app_is_active
}

needs_cold_start() {
    [ "$WARM_START" -eq 0 ] && ! using_second_instance_handoff
}

detect_warm_start() {
    local pid

    if pid="$(find_running_app_pid)"; then
        set_detected_running_app "$pid"
        return 0
    fi

    if runtime_recovery_scan_needed && pid="$(discover_running_app_pid)"; then
        set_detected_running_app "$pid"
        return 0
    fi

    if ! linux_setting_enabled "codex-linux-warm-start-enabled" 1; then
        WARM_START=0
        echo "Warm-start handoff disabled by $APP_SETTINGS_FILE"
    fi
}

running_app_uses_renderer_url_override() {
    local allow_override
    local renderer_url

    running_app_is_active || return 1
    allow_override="$(pid_environ_value "$RUNNING_APP_PID" CODEX_LINUX_ALLOW_RENDERER_URL_OVERRIDE 2>/dev/null || true)"
    truthy_env_value "$allow_override" || return 1
    renderer_url="$(pid_environ_value "$RUNNING_APP_PID" ELECTRON_RENDERER_URL 2>/dev/null || true)"
    [ -n "$renderer_url" ] && [ "$renderer_url" != "$WEBVIEW_ORIGIN/" ]
}

terminate_stale_electron_with_pidfd() {
    local pid="$1"
    local expected_start_time

    pid_matches_executable "$pid" "$SCRIPT_DIR/electron" || return 1
    expected_start_time="$(pid_start_time "$pid")" || return 1

    python3 - \
        "$pid" \
        "$expected_start_time" \
        "$SCRIPT_DIR/electron" \
        "$CODEX_LINUX_APP_ID" \
        "$CODEX_LINUX_INSTANCE_ID" \
        "$MULTI_LAUNCH_ACTIVE" <<'PY'
import os
import select
import signal
import sys

pid = int(sys.argv[1])
expected_start_time = sys.argv[2]
expected_executable = sys.argv[3]
expected_app_id = sys.argv[4]
expected_instance_id = sys.argv[5]
expected_multi_launch = sys.argv[6] == "1"

if not hasattr(os, "pidfd_open") or not hasattr(signal, "pidfd_send_signal"):
    print("pidfd APIs are unavailable; refusing to terminate the stale Electron process", file=sys.stderr)
    sys.exit(1)

try:
    pidfd = os.pidfd_open(pid, 0)
except OSError as exc:
    print(f"could not open pidfd for stale Electron pid {pid}: {exc}", file=sys.stderr)
    sys.exit(1)

try:
    with open(f"/proc/{pid}/stat", "r", encoding="utf-8") as handle:
        stat_fields = handle.read().rsplit(") ", 1)[1].split()
    actual_start_time = stat_fields[19]
    with open(f"/proc/{pid}/status", "r", encoding="utf-8") as handle:
        status_lines = handle.read().splitlines()
    uid_line = next(line for line in status_lines if line.startswith("Uid:"))
    actual_uid = int(uid_line.split()[1])
    with open(f"/proc/{pid}/cmdline", "rb") as handle:
        cmdline = [part for part in handle.read().split(b"\0") if part]
    with open(f"/proc/{pid}/environ", "rb") as handle:
        environ_entries = [part for part in handle.read().split(b"\0") if b"=" in part]
except (OSError, IndexError, StopIteration, ValueError) as exc:
    print(f"could not revalidate stale Electron pid {pid}: {exc}", file=sys.stderr)
    os.close(pidfd)
    sys.exit(1)

environ = {}
for entry in environ_entries:
    key, value = entry.split(b"=", 1)
    environ[os.fsdecode(key)] = os.fsdecode(value)

identity_matches = (
    actual_start_time == expected_start_time
    and actual_uid == os.geteuid()
    and bool(cmdline)
    and os.fsdecode(cmdline[0]) == expected_executable
    and not any(part.startswith(b"--type=") for part in cmdline[1:])
    and expected_app_id in {
        environ.get("CODEX_LINUX_APP_ID"),
        environ.get("CODEX_APP_ID"),
    }
)
if expected_multi_launch:
    identity_matches = identity_matches and environ.get("CODEX_LINUX_INSTANCE_ID") == expected_instance_id
else:
    identity_matches = identity_matches and environ.get("CODEX_LINUX_MULTI_LAUNCH") != "1"

if not identity_matches:
    print(f"stale Electron pid {pid} failed identity revalidation", file=sys.stderr)
    os.close(pidfd)
    sys.exit(1)

poller = select.poll()
poller.register(pidfd, select.POLLIN)
try:
    signal.pidfd_send_signal(pidfd, signal.SIGTERM)
except ProcessLookupError:
    os.close(pidfd)
    sys.exit(0)
except OSError as exc:
    print(f"could not terminate stale Electron pid {pid}: {exc}", file=sys.stderr)
    os.close(pidfd)
    sys.exit(1)

if not poller.poll(2000):
    try:
        signal.pidfd_send_signal(pidfd, signal.SIGKILL)
    except ProcessLookupError:
        pass
    except OSError as exc:
        print(f"could not kill stale Electron pid {pid}: {exc}", file=sys.stderr)
        os.close(pidfd)
        sys.exit(1)
    if not poller.poll(1000):
        print(f"stale Electron pid {pid} did not exit", file=sys.stderr)
        os.close(pidfd)
        sys.exit(1)

os.close(pidfd)
PY
}

recover_unhealthy_running_app() {
    running_app_is_active || return 0

    if running_app_uses_renderer_url_override; then
        echo "Skipping packaged webview recovery because an explicit renderer URL override is active"
        return 0
    fi
    webview_origin_is_reachable && return 0

    if webview_port_is_open; then
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME packaged webview origin is unhealthy, but port $CODEX_LINUX_WEBVIEW_PORT is occupied. Keeping the live Electron process untouched. Close it manually and retry."
        exit 1
    fi

    echo "Detected live Electron pid=$RUNNING_APP_PID with an unavailable packaged webview origin; preparing a safe cold-start recovery"
    acquire_launcher_lock
    if [ "$LAUNCHER_LOCK_HELD" -ne 1 ] || [ "$LAUNCHER_LOCK_TIMED_OUT" -eq 1 ]; then
        release_launcher_lock
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME could not lock launcher state for safe webview recovery. Keeping the live Electron process untouched; close it manually and retry."
        exit 1
    fi

    refresh_launch_state_quick
    if ! running_app_is_active; then
        WARM_START=0
        reconcile_runtime_state
        return 0
    fi
    if running_app_uses_renderer_url_override; then
        release_launcher_lock
        echo "Skipping packaged webview recovery because the running app uses an explicit renderer URL override"
        return 0
    fi
    if webview_origin_is_reachable; then
        release_launcher_lock
        echo "Packaged webview origin recovered while waiting for launcher state"
        return 0
    fi
    if webview_port_is_open; then
        release_launcher_lock
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME packaged webview port became occupied during recovery. Keeping the live Electron process untouched; close it manually and retry."
        exit 1
    fi

    if ! terminate_stale_electron_with_pidfd "$RUNNING_APP_PID"; then
        release_launcher_lock
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME could not safely verify and stop stale Electron pid $RUNNING_APP_PID. Close it manually and retry."
        exit 1
    fi

    echo "Stopped identity-verified stale Electron pid=$RUNNING_APP_PID; continuing with a cold start"
    WARM_START=0
    RUNNING_APP_PID=""
    rm -f "$APP_PID_FILE" "$WEBVIEW_PID_FILE" "$LAUNCH_ACTION_SOCKET"
}

send_warm_start_launch_action() {
    [ "$WARM_START" -eq 1 ] || return 1
    [ -S "$LAUNCH_ACTION_SOCKET" ] || return 1

    python3 - "$LAUNCH_ACTION_SOCKET" "$@" <<'PY'
import json
import socket
import sys

socket_path = sys.argv[1]
argv = sys.argv[2:]
payload = json.dumps({"argv": argv}, separators=(",", ":")).encode("utf-8") + b"\n"

client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.settimeout(1.0)
try:
    client.connect(socket_path)
    client.sendall(payload)
    client.shutdown(socket.SHUT_WR)
    response = client.recv(32)
    if not response.startswith(b"ok\n"):
        sys.exit(1)
finally:
    client.close()
PY
}

webview_origin_is_reachable() {
    verify_webview_origin "$WEBVIEW_ORIGIN/index.html" >/dev/null 2>&1
}

webview_origin_is_reachable_fast() {
    local url="$WEBVIEW_ORIGIN/index.html"
    local body err

    err=$(mktemp 2>/dev/null) || err="${TMPDIR:-/tmp}/codex-curl-err.$$"
    if ! body="$(curl --disable --noproxy 127.0.0.1,localhost --silent --show-error --fail --max-time 0.2 "$url" 2>"$err")"; then
        rm -f "$err"
        return 1
    fi
    rm -f "$err"

    [[ "$body" == *"<title>Codex</title>"* ]] || return 1
    [[ "$body" == *"startup-loader"* ]] || return 1
}

webview_port_is_open() {
    # bash /dev/tcp connect, capped at 0.2 s by a watchdog kill.
    # Replaces python3 socket.connect heredoc (~3 ms vs 13–60 ms here).
    # /usr/bin/timeout is intentionally not used: on uutils-coreutils
    # distros (Ubuntu 26+) its cold start is ~105 ms, which would dominate
    # the 200 ms cap it would enforce.
    local probe_pid kill_pid rc=1 self_pid=$$
    ( trap - EXIT
      exec 3<>/dev/tcp/127.0.0.1/"$CODEX_LINUX_WEBVIEW_PORT" || exit 1
      exec 3>&- 3<&-
      exit 0 ) 2>/dev/null &
    probe_pid=$!
    # Watchdog: before SIGKILL, verify /proc/<probe>/stat still reports us as
    # the parent. Guards against PID reuse: if the probe has already exited
    # and the kernel has recycled that PID to an unrelated process between
    # the wait below and the watchdog firing, its ppid will not match
    # $self_pid and the SIGKILL is suppressed.
    ( trap - EXIT
      sleep 0.2
      [ -r "/proc/$probe_pid/stat" ] || exit 0
      # /proc/<pid>/stat: "<pid> (comm) <state> <ppid> ..." — comm can contain
      # spaces/parens, so strip up to the last ") " before splitting fields.
      local stat_line rest
      stat_line=$(< "/proc/$probe_pid/stat") || exit 0
      rest=${stat_line##*) }
      set -- $rest
      # $1 = state, $2 = ppid
      [ "${2:-}" = "$self_pid" ] && kill -9 "$probe_pid" 2>/dev/null
    ) 2>/dev/null &
    kill_pid=$!
    if wait "$probe_pid" 2>/dev/null; then rc=0; fi
    kill "$kill_pid" 2>/dev/null
    wait "$kill_pid" 2>/dev/null
    return $rc
}

wait_for_webview_server() {
    echo "Waiting for webview server on :$CODEX_LINUX_WEBVIEW_PORT"

    # Treat the actual localhost webview response as the readiness signal.
    # Under cold-login load the shell /dev/tcp probe can be a little too brittle,
    # while a short curl against index.html matches what Electron needs next.
    local attempt
    for attempt in $(seq 1 20); do
        if webview_origin_is_reachable_fast; then
            echo "Webview server is ready"
            return 0
        fi
        sleep 0.05
    done

    # The fast probe caps curl at 0.2s; a valid local webview can answer a little
    # slower during cold start. Confirm with the full 2s origin check before failing.
    if webview_origin_is_reachable; then
        echo "Webview server is ready"
        return 0
    fi

    return 1
}

verify_webview_origin() {
    local url="$1"

    # curl with built-in --max-time replaces python3 urllib (~7 ms vs ~38 ms).
    # --fail rejects non-2xx; --max-time 2 mirrors urllib.urlopen(timeout=2).
    # --show-error + 2>$err captures curl's actual diagnostic so failures
    # surface the underlying cause (HTTP status, connect refused, timeout,
    # SSL error, etc.) instead of a generic "HTTP fetch failed", matching
    # what the original python3 urllib version exposed via its raised
    # exception text.
    # --noproxy keeps loopback validation pointed at the embedded webview
    # server even when the user's shell exports http_proxy/ALL_PROXY without a
    # localhost NO_PROXY exemption.
    local body err detail
    err=$(mktemp 2>/dev/null) || err="${TMPDIR:-/tmp}/codex-curl-err.$$"
    if ! body="$(curl --disable --noproxy 127.0.0.1,localhost --silent --show-error --fail --max-time 2 "$url" 2>"$err")"; then
        detail=$(cat "$err" 2>/dev/null || true)
        rm -f "$err"
        echo "Webview origin validation failed for $url; ${detail:-curl exited non-zero with no diagnostic}" >&2
        return 1
    fi
    rm -f "$err"

    local missing=()
    [[ "$body" == *"<title>Codex</title>"* ]] || missing+=("<title>Codex</title>")
    [[ "$body" == *"startup-loader"* ]]       || missing+=("startup-loader")
    if [ ${#missing[@]} -gt 0 ]; then
        echo "Webview origin validation failed for $url; missing markers: ${missing[*]}" >&2
        return 1
    fi
}

pid_has_webview_server_cmdline() {
    local pid="$1"
    local cmdline

    [[ "$pid" =~ ^[0-9]+$ ]] || return 1
    [ -d "/proc/$pid" ] || return 1
    pid_is_current_user "$pid" || return 1
    cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
    [[ "$cmdline" == *"http.server $CODEX_LINUX_WEBVIEW_PORT"* || "$cmdline" == *"webview-server.py $CODEX_LINUX_WEBVIEW_PORT"* ]]
}

pid_is_webview_server() {
    local pid="$1"
    local cwd

    pid_has_webview_server_cmdline "$pid" || return 1
    cwd="$(readlink -f "/proc/$pid/cwd" 2>/dev/null || true)"
    [ "$cwd" = "$(canonical_path "$WEBVIEW_DIR")" ]
}

pid_is_stale_webview_server() {
    local pid="$1"
    local cwd
    local current_webview_dir
    local deleted_webview_dir

    pid_has_webview_server_cmdline "$pid" || return 1
    cwd="$(readlink -f "/proc/$pid/cwd" 2>/dev/null || true)"
    [ -n "$cwd" ] || return 1

    current_webview_dir="$(canonical_path "$WEBVIEW_DIR")"
    deleted_webview_dir="$current_webview_dir (deleted)"
    [ "$cwd" = "$deleted_webview_dir" ] && return 0

    [ "$cwd" != "$current_webview_dir" ]
}

stop_owned_webview_server() {
    local pid=""

    if [ -f "$WEBVIEW_PID_FILE" ]; then
        pid="$(cat "$WEBVIEW_PID_FILE" 2>/dev/null || true)"
    fi

    if running_app_is_active && [ -n "$pid" ] && pid_is_webview_server "$pid"; then
        echo "Preserving webview server pid=$pid owned by running ChatGPT Desktop pid=$RUNNING_APP_PID"
        return 0
    fi

    if [ -n "$pid" ] && { pid_is_webview_server "$pid" || pid_is_stale_webview_server "$pid"; }; then
        echo "Stopping owned webview server pid=$pid"
        kill "$pid" 2>/dev/null || true
        for _ in $(seq 1 20); do
            kill -0 "$pid" 2>/dev/null || break
            sleep 0.05
        done
    fi

    rm -f "$WEBVIEW_PID_FILE"
}

owned_webview_server_pid() {
    local pid=""

    if [ -f "$WEBVIEW_PID_FILE" ]; then
        pid="$(cat "$WEBVIEW_PID_FILE" 2>/dev/null || true)"
    fi

    if [ -n "$pid" ] && pid_is_webview_server "$pid"; then
        echo "$pid"
        return 0
    fi

    if [ -n "$pid" ]; then
        rm -f "$WEBVIEW_PID_FILE"
    fi

    return 1
}

discover_webview_server_pid() {
    local proc_cmdline
    local pid

    for proc_cmdline in /proc/[0-9]*/cmdline; do
        [ -e "$proc_cmdline" ] || continue
        pid="${proc_cmdline#/proc/}"
        pid="${pid%/cmdline}"
        if pid_is_webview_server "$pid"; then
            echo "$pid"
            return 0
        fi
    done

    return 1
}

stale_webview_server_pid() {
    local proc_cmdline
    local pid

    for proc_cmdline in /proc/[0-9]*/cmdline; do
        [ -e "$proc_cmdline" ] || continue
        pid="${proc_cmdline#/proc/}"
        pid="${pid%/cmdline}"
        if pid_is_stale_webview_server "$pid"; then
            echo "$pid"
            return 0
        fi
    done

    return 1
}

stop_stale_webview_server() {
    local pid

    running_app_is_active && return 0

    while pid="$(stale_webview_server_pid)"; do
        echo "Stopping stale webview server pid=$pid cwd=$(readlink -f "/proc/$pid/cwd" 2>/dev/null || true)"
        kill "$pid" 2>/dev/null || true
        for _ in $(seq 1 20); do
            kill -0 "$pid" 2>/dev/null || break
            sleep 0.05
        done
    done
}

adopt_existing_webview_server() {
    local pid

    if pid="$(owned_webview_server_pid)"; then
        if running_app_is_active; then
            ADOPTED_WEBVIEW_PID="$pid"
            echo "Reusing webview server pid=$pid owned by running ChatGPT Desktop pid=$RUNNING_APP_PID"
        else
            STARTED_WEBVIEW_PID="$pid"
        fi
        return 0
    fi

    return 1
}

adopt_discovered_webview_server() {
    local pid

    if pid="$(discover_webview_server_pid)"; then
        echo "$pid" > "$WEBVIEW_PID_FILE"
        if running_app_is_active; then
            ADOPTED_WEBVIEW_PID="$pid"
            echo "Reusing webview server pid=$pid owned by running ChatGPT Desktop pid=$RUNNING_APP_PID"
        else
            STARTED_WEBVIEW_PID="$pid"
            echo "Adopted existing webview server pid=$pid dir=$WEBVIEW_DIR"
        fi
        return 0
    fi

    return 1
}

renderer_url_override_is_active() {
    if truthy_env_value "${CODEX_LINUX_ALLOW_RENDERER_URL_OVERRIDE:-}" &&
       [ -n "${ELECTRON_RENDERER_URL:-}" ] &&
       [ "$ELECTRON_RENDERER_URL" != "$WEBVIEW_ORIGIN/" ]; then
        return 0
    fi

    return 1
}

require_webview_entrypoint() {
    local webview_index="$WEBVIEW_DIR/index.html"

    if [ ! -f "$webview_index" ]; then
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME webview bundle is incomplete: missing $webview_index. Rebuild the app with ./install.sh before launching."
        exit 1
    fi
}

start_webview_server() {
    WEBVIEW_STARTUP_PENDING=0

    if renderer_url_override_is_active; then
        echo "Skipping packaged webview setup because ELECTRON_RENDERER_URL override is enabled"
        log_phase "webview_override"
        return 0
    fi

    require_webview_entrypoint

    if adopt_existing_webview_server; then
        if webview_origin_is_reachable; then
            echo "Reusing existing verified webview server on :$CODEX_LINUX_WEBVIEW_PORT"
            log_phase "webview_reused"
            return 0
        fi

        if running_app_is_active; then
            notify_error "ChatGPT Desktop webview server is already running for pid $RUNNING_APP_PID, but origin validation failed. Keeping the live app untouched."
            exit 1
        fi
    fi

    if webview_port_is_open; then
        stop_stale_webview_server
    fi

    if webview_port_is_open && webview_origin_is_reachable; then
        if adopt_discovered_webview_server; then
            echo "Reusing existing verified webview server on :$CODEX_LINUX_WEBVIEW_PORT"
            log_phase "webview_reused"
            return 0
        fi

        detect_cross_install_conflict
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME webview port $CODEX_LINUX_WEBVIEW_PORT is already serving Codex content, but it is not owned by this launcher. Stop the other webview server and try again."
        exit 1
    fi

    if webview_port_is_open; then
        stop_stale_webview_server
        if webview_origin_is_reachable; then
            if adopt_discovered_webview_server; then
                echo "Reusing existing verified webview server on :$CODEX_LINUX_WEBVIEW_PORT"
                log_phase "webview_reused"
                return 0
            fi
        fi

        if webview_port_is_open; then
            detect_cross_install_conflict
            notify_error "$CODEX_LINUX_APP_DISPLAY_NAME webview port $CODEX_LINUX_WEBVIEW_PORT is already in use. Stop the other process and try again."
            exit 1
        fi
    fi

    stop_owned_webview_server

    cd "$WEBVIEW_DIR"
    python3 "$SCRIPT_DIR/.codex-linux/webview-server.py" "$CODEX_LINUX_WEBVIEW_PORT" --bind 127.0.0.1 &
    STARTED_WEBVIEW_PID=$!
    echo "$STARTED_WEBVIEW_PID" > "$WEBVIEW_PID_FILE"
    WEBVIEW_STARTUP_PENDING=1

    echo "Started webview server pid=$STARTED_WEBVIEW_PID dir=$WEBVIEW_DIR"
    log_phase "webview_spawned"
}

# Readiness runs separately from the spawn so the Python server finishes
# booting while the launcher does unrelated cold-start work (CLI lookup,
# plugin cache syncs). Electron never launches before this verifies the
# origin; the server is tied to the launcher lifetime via PR_SET_PDEATHSIG,
# so early-exit paths cannot leak it.
await_webview_server_ready() {
    [ "${WEBVIEW_STARTUP_PENDING:-0}" -eq 1 ] || return 0
    WEBVIEW_STARTUP_PENDING=0

    if ! wait_for_webview_server; then
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME webview server did not become ready on port $CODEX_LINUX_WEBVIEW_PORT. Check the launcher log for the embedded http.server output."
        exit 1
    fi

    if ! kill -0 "$STARTED_WEBVIEW_PID" 2>/dev/null; then
        if webview_origin_is_reachable && adopt_discovered_webview_server; then
            echo "Adopted concurrently-started verified webview server on :$CODEX_LINUX_WEBVIEW_PORT"
            log_phase "webview_reused"
            return 0
        fi

        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME webview server exited before Electron launch. Another process may already be using port $CODEX_LINUX_WEBVIEW_PORT."
        exit 1
    fi

    if ! verify_webview_origin "$WEBVIEW_ORIGIN/index.html"; then
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME webview origin validation failed. Another process may be serving port $CODEX_LINUX_WEBVIEW_PORT or the extracted webview bundle is incomplete."
        exit 1
    fi

    echo "Webview origin verified."
    log_phase "webview_ready"
}

clear_stale_pid_file() {
    if [ ! -f "$APP_PID_FILE" ]; then
        return 0
    fi

    local pid=""
    pid="$(cat "$APP_PID_FILE" 2>/dev/null || true)"
    if [ -z "$pid" ] || ! pid_matches_executable "$pid" "$SCRIPT_DIR/electron"; then
        rm -f "$APP_PID_FILE"
    fi
}

reconcile_runtime_state() {
    local webview_pid=""

    if [ -n "${RUNNING_APP_PID:-}" ] && running_app_is_active; then
        echo "$RUNNING_APP_PID" > "$APP_PID_FILE"
        return 0
    fi

    clear_stale_pid_file

    if [ -e "$LAUNCH_ACTION_SOCKET" ]; then
        rm -f "$LAUNCH_ACTION_SOCKET"
    fi

    if [ ! -f "$WEBVIEW_PID_FILE" ]; then
        return 0
    fi

    webview_pid="$(cat "$WEBVIEW_PID_FILE" 2>/dev/null || true)"
    if [ -z "$webview_pid" ] || { ! pid_is_webview_server "$webview_pid" && ! pid_is_stale_webview_server "$webview_pid"; }; then
        rm -f "$WEBVIEW_PID_FILE"
    fi
}

is_wsl_environment() {
    if [ -n "${WSL_INTEROP:-}" ] || [ -n "${WSL_DISTRO_NAME:-}" ]; then
        return 0
    fi

    grep -qiE "(microsoft|wsl)" /proc/sys/kernel/osrelease 2>/dev/null
}

is_wslg_session() {
    is_wsl_environment || return 1

    if [ -n "${WAYLAND_DISPLAY:-}" ]; then
        return 0
    fi

    [ -n "${DISPLAY:-}" ] && [ -e /mnt/wslg ]
}

gnome_desktop_detected() {
    case "${XDG_CURRENT_DESKTOP:-}:${XDG_SESSION_DESKTOP:-}:${DESKTOP_SESSION:-}" in
        *GNOME*|*gnome*) return 0 ;;
        *) return 1 ;;
    esac
}

connected_drm_monitor_count() {
    local drm_root="${CODEX_DRM_CLASS_ROOT:-/sys/class/drm}"
    local status_file status count=0

    [ -d "$drm_root" ] || return 1

    for status_file in "$drm_root"/*/status; do
        [ -f "$status_file" ] || continue
        IFS= read -r status < "$status_file" || continue
        [ "$status" = "connected" ] || continue
        count=$((count + 1))
    done

    [ "$count" -gt 0 ] || return 1
    printf '%s\n' "$count"
}

gnome_wayland_multi_monitor_detected() {
    local count

    [ "${XDG_SESSION_TYPE:-}" = "wayland" ] || return 1
    gnome_desktop_detected || return 1
    count="$(connected_drm_monitor_count)" || return 1
    [ "$count" -gt 1 ]
}

normalize_linux_rendering_mode() {
    case "${CODEX_LINUX_RENDERING_MODE:-auto}" in
        auto|default|wslg|wayland-gpu)
            echo "${CODEX_LINUX_RENDERING_MODE:-auto}"
            ;;
        *)
            echo "Invalid CODEX_LINUX_RENDERING_MODE='${CODEX_LINUX_RENDERING_MODE:-}'; using auto" >&2
            echo "auto"
            ;;
    esac
}

truthy_env_value() {
    case "${1:-}" in
        1|true|TRUE|yes|YES|on|ON) return 0 ;;
        *) return 1 ;;
    esac
}

falsey_env_value() {
    case "${1:-}" in
        0|false|FALSE|no|NO|off|OFF) return 0 ;;
        *) return 1 ;;
    esac
}

scan_electron_rendering_arg() {
    case "$1" in
        --ozone-platform=*)
            ELECTRON_OZONE_SWITCH_IN_ARGS=1
            ELECTRON_OZONE_PLATFORM_ARG="${1#--ozone-platform=}"
            ;;
        --ozone-platform|--ozone-platform-hint|--ozone-platform-hint=*)
            ELECTRON_OZONE_SWITCH_IN_ARGS=1
            ;;
        --use-gl|--use-gl=*|--use-angle|--use-angle=*)
            ELECTRON_GL_SWITCH_PROVIDED=1
            ;;
        --disable-gpu|--disable-gpu=*)
            ELECTRON_GPU_DISABLE_SWITCH_IN_ARGS=1
            ;;
        --disable-gpu-compositing|--disable-gpu-compositing=*)
            ELECTRON_GPU_COMPOSITING_SWITCH_PROVIDED=1
            ;;
        --force-device-scale-factor|--force-device-scale-factor=*)
            ELECTRON_SCALE_SWITCH_IN_ARGS=1
            ;;
    esac
}

codex_forced_device_scale_factor() {
    local value="${CODEX_FORCE_DEVICE_SCALE_FACTOR:-}"

    [ -n "$value" ] || return 1
    if [[ ! "$value" =~ ^[0-9]+(\.[0-9]+)?$ ]] || [[ "$value" =~ ^0+(\.0+)?$ ]]; then
        echo "Invalid CODEX_FORCE_DEVICE_SCALE_FACTOR='$value'; expected a positive number like 1, 1.25, or 2" >&2
        return 1
    fi

    printf '%s\n' "$value"
}

apply_codex_ozone_platform_env() {
    local value="${CODEX_OZONE_PLATFORM:-}"

    [ -n "$value" ] || return 0
    if [ "$ELECTRON_PLATFORM_EXPLICIT" -eq 1 ] || [ "$ELECTRON_OZONE_SWITCH_IN_ARGS" -eq 1 ]; then
        echo "Ignoring CODEX_OZONE_PLATFORM='$value'; an explicit platform flag was provided"
        return 0
    fi

    case "$value" in
        x11|wayland)
            ELECTRON_OZONE_PLATFORM="$value"
            ELECTRON_OZONE_HINT=""
            ELECTRON_PLATFORM_EXPLICIT=1
            ;;
        auto)
            ELECTRON_OZONE_PLATFORM=""
            ELECTRON_OZONE_HINT="auto"
            ELECTRON_PLATFORM_EXPLICIT=1
            ;;
        *)
            echo "Invalid CODEX_OZONE_PLATFORM='$value'; expected x11, wayland, or auto" >&2
            ;;
    esac
}

add_electron_enable_features() {
    local raw="$1"
    local feature
    local existing
    local IFS=,
    local parts=()

    read -r -a parts <<< "$raw"
    for feature in "${parts[@]}"; do
        feature="${feature#"${feature%%[![:space:]]*}"}"
        feature="${feature%"${feature##*[![:space:]]}"}"
        [ -n "$feature" ] || continue
        for existing in "${ELECTRON_ENABLE_FEATURES[@]}"; do
            [ "$existing" != "$feature" ] || continue 2
        done
        ELECTRON_ENABLE_FEATURES+=("$feature")
    done
}

electron_enable_features_arg() {
    local joined=""
    local feature

    for feature in "${ELECTRON_ENABLE_FEATURES[@]}"; do
        if [ -z "$joined" ]; then
            joined="$feature"
        else
            joined="$joined,$feature"
        fi
    done

    [ -n "$joined" ] || return 1
    printf '%s\n' "--enable-features=$joined"
}

write_user_electron_flags_template() {
    cat > "$USER_ELECTRON_FLAGS_FILE" <<'EOF'
# ChatGPT Desktop for Linux launch flags.
# Uncomment one flag per line. Existing files are never overwritten.
# Lines starting with # and blank lines are ignored.
# Shell syntax, quotes, variables, and inline comments are not evaluated.
#
# X11 / Wayland:
# --x11
# --wayland
# --ozone-platform=x11
# --ozone-platform=wayland
# --ozone-platform-hint=auto
#
# Wayland / IME:
# --enable-wayland-ime
# --wayland-text-input-version=1
#
# GPU / rendering:
# --safe-mode
# --disable-gpu
# --enable-gpu
# --disable-gpu-compositing
# --use-gl=angle
# --use-gl=desktop
# --use-angle=gl
# --disable-features=Vulkan
#
# Accessibility:
# --force-renderer-accessibility
#
# Wayland decorations:
# --enable-features=WaylandWindowDecorations
#
# HiDPI / fractional scaling (oversized or blurry UI):
# --force-device-scale-factor=1
# --force-device-scale-factor=1.5
EOF
}

ensure_user_electron_flags_file() {
    [ -n "${USER_ELECTRON_FLAGS_FILE:-}" ] || return 0
    if [ -e "$USER_ELECTRON_FLAGS_FILE" ] || [ -L "$USER_ELECTRON_FLAGS_FILE" ]; then
        return 0
    fi

    mkdir -p "$(dirname "$USER_ELECTRON_FLAGS_FILE")"
    if write_user_electron_flags_template; then
        echo "Created persistent Electron flags template: $USER_ELECTRON_FLAGS_FILE"
    else
        echo "Could not create persistent Electron flags template: $USER_ELECTRON_FLAGS_FILE"
        rm -f "$USER_ELECTRON_FLAGS_FILE" 2>/dev/null || true
    fi
}

load_user_electron_flags() {
    USER_ELECTRON_FLAGS=()
    [ -n "${USER_ELECTRON_FLAGS_FILE:-}" ] || return 0
    [ -f "$USER_ELECTRON_FLAGS_FILE" ] || return 0

    local line
    while IFS= read -r line || [ -n "$line" ]; do
        line="${line%$'\r'}"
        case "$line" in
            ""|\#*) continue ;;
            --)
                echo "Ignoring unsupported separator in Electron flags file: $USER_ELECTRON_FLAGS_FILE"
                continue
                ;;
        esac
        USER_ELECTRON_FLAGS+=("$line")
    done < "$USER_ELECTRON_FLAGS_FILE"

    if [ "${#USER_ELECTRON_FLAGS[@]}" -gt 0 ]; then
        echo "Loaded ${#USER_ELECTRON_FLAGS[@]} persistent Electron flag(s): $USER_ELECTRON_FLAGS_FILE"
    fi
}

apply_electron_rendering_profile() {
    local requested_mode
    requested_mode="$(normalize_linux_rendering_mode)"

    ELECTRON_RENDERING_MODE="$requested_mode"
    ELECTRON_WSLG_DETECTED=0
    if is_wslg_session; then
        ELECTRON_WSLG_DETECTED=1
    fi

    case "$requested_mode" in
        auto)
            if [ "$ELECTRON_WSLG_DETECTED" -eq 1 ]; then
                ELECTRON_RENDERING_MODE="wslg"
            elif [ "$ELECTRON_PLATFORM_EXPLICIT" -eq 0 ] &&
                [ "$ELECTRON_OZONE_SWITCH_IN_ARGS" -eq 0 ] &&
                gnome_wayland_multi_monitor_detected; then
                ELECTRON_RENDERING_MODE="gnome-wayland-multi-monitor"
            else
                ELECTRON_RENDERING_MODE="default"
            fi
            ;;
        default)
            return 0
            ;;
        wayland-gpu)
            if [ "$ELECTRON_PLATFORM_EXPLICIT" -eq 0 ] && [ "$ELECTRON_OZONE_SWITCH_IN_ARGS" -eq 0 ]; then
                ELECTRON_OZONE_PLATFORM="wayland"
                ELECTRON_OZONE_HINT=""
            fi
            return 0
            ;;
    esac

    if [ "$ELECTRON_RENDERING_MODE" = "gnome-wayland-multi-monitor" ]; then
        ELECTRON_OZONE_PLATFORM="x11"
        ELECTRON_OZONE_HINT=""
        echo "Detected GNOME Wayland multi-monitor session; forcing --ozone-platform=x11 for stable window scale/maximize behavior. Set CODEX_OZONE_PLATFORM=wayland or CODEX_LINUX_RENDERING_MODE=default to opt out."
        return 0
    fi

    [ "$ELECTRON_RENDERING_MODE" = "wslg" ] || return 0

    if [ "$ELECTRON_PLATFORM_EXPLICIT" -eq 0 ] && [ "$ELECTRON_OZONE_SWITCH_IN_ARGS" -eq 0 ]; then
        ELECTRON_OZONE_PLATFORM="x11"
        ELECTRON_OZONE_HINT=""
    fi

    if [ "$ELECTRON_GL_SWITCH_PROVIDED" -eq 0 ] && [ "$ELECTRON_GPU_ENABLED" = "1" ] && [ "$ELECTRON_GPU_DISABLE_SWITCH_IN_ARGS" -eq 0 ]; then
        ELECTRON_ARGS+=(--use-gl=angle)
        ELECTRON_GL_SWITCH_PROVIDED=1
        ELECTRON_GL_SWITCH_ADDED=1
    fi
}

# --disable-dev-shm-usage exists for containers whose /dev/shm is too small
# for Chromium's renderer/GPU shared-memory transport (Docker defaults to
# 64 MiB). Everywhere else the flag pushes those buffers onto disk-backed
# temp storage and hurts rendering performance, so it is only kept when
# /dev/shm is missing, not writable, or below 1 GiB.
dev_shm_size_kb() {
    local size_kb
    size_kb="$(df -Pk /dev/shm 2>/dev/null | awk 'NR == 2 { print $2 }')"
    case "$size_kb" in
        ""|*[!0-9]*)
            return 1
            ;;
    esac
    printf '%s\n' "$size_kb"
}

should_disable_dev_shm_usage() {
    local min_size_kb=1048576 size_kb

    if [ -n "${CODEX_ELECTRON_DISABLE_DEV_SHM_USAGE:-}" ]; then
        if truthy_env_value "$CODEX_ELECTRON_DISABLE_DEV_SHM_USAGE"; then
            return 0
        fi
        if falsey_env_value "$CODEX_ELECTRON_DISABLE_DEV_SHM_USAGE"; then
            return 1
        fi
        case "$CODEX_ELECTRON_DISABLE_DEV_SHM_USAGE" in
            auto|AUTO|Auto) ;;
            *)
                echo "Invalid CODEX_ELECTRON_DISABLE_DEV_SHM_USAGE='${CODEX_ELECTRON_DISABLE_DEV_SHM_USAGE:-}'; using automatic /dev/shm detection" >&2
                ;;
        esac
    fi

    if [ ! -d /dev/shm ] || [ ! -w /dev/shm ]; then
        return 0
    fi

    if ! size_kb="$(dev_shm_size_kb)"; then
        return 0
    fi

    if [ "$size_kb" -lt "$min_size_kb" ]; then
        echo "Detected small /dev/shm (${size_kb} KiB); keeping --disable-dev-shm-usage. Set CODEX_ELECTRON_DISABLE_DEV_SHM_USAGE=0 to opt out."
        return 0
    fi

    return 1
}

should_disable_gpu_compositing() {
    if [ "$ELECTRON_GPU_COMPOSITING_SWITCH_PROVIDED" -eq 1 ]; then
        return 0
    fi

    if [ -n "${CODEX_ELECTRON_DISABLE_GPU_COMPOSITING:-}" ]; then
        if truthy_env_value "$CODEX_ELECTRON_DISABLE_GPU_COMPOSITING"; then
            return 0
        fi
        if falsey_env_value "$CODEX_ELECTRON_DISABLE_GPU_COMPOSITING"; then
            return 1
        fi
        echo "Invalid CODEX_ELECTRON_DISABLE_GPU_COMPOSITING='${CODEX_ELECTRON_DISABLE_GPU_COMPOSITING:-}'; using rendering profile default" >&2
    fi

    if [ "${XDG_SESSION_TYPE:-}" = "wayland" ] &&
        [ "$ELECTRON_RENDERING_MODE" != "wayland-gpu" ] &&
        [ "$ELECTRON_RENDERING_MODE" != "wslg" ]; then
        echo "Detected Wayland session; adding --disable-gpu-compositing for side-panel rendering stability. Set CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=0 to opt out."
        return 0
    fi

    return 1
}

effective_electron_ozone_platform() {
    if [ -n "$ELECTRON_OZONE_PLATFORM" ]; then
        echo "$ELECTRON_OZONE_PLATFORM"
        return 0
    fi

    if [ -n "$ELECTRON_OZONE_PLATFORM_ARG" ]; then
        echo "$ELECTRON_OZONE_PLATFORM_ARG"
    fi
}

# --force-renderer-accessibility keeps Chromium's accessibility engine active
# in every renderer, so each DOM update also rebuilds and serializes the
# accessibility tree — a measurable slowdown for the complex webview UI. Only
# pay that cost when an assistive technology is plausibly in use; anything
# missed by this detection can set CODEX_FORCE_RENDERER_ACCESSIBILITY=1.
assistive_technology_detected() {
    local env_marker

    for env_marker in "${GNOME_ACCESSIBILITY:-}" "${QT_LINUX_ACCESSIBILITY_ALWAYS_ON:-}" "${ACCESSIBILITY_ENABLED:-}"; do
        if truthy_env_value "$env_marker"; then
            return 0
        fi
    done

    if command -v pgrep >/dev/null 2>&1; then
        if pgrep -x orca >/dev/null 2>&1 || pgrep -x brltty >/dev/null 2>&1; then
            return 0
        fi
    fi

    session_bus_assistive_technology_detected
}

# Session-bus accessibility signals: the GNOME screen-reader setting plus the
# AT-SPI state that `codex-computer-use-linux setup` enables (org.a11y.Status
# IsEnabled via busctl, with org.gnome.desktop.interface toolkit-accessibility
# as its gsettings fallback). gsettings/busctl can hang on a broken session
# bus, so each external helper runs under a watchdog capped at 0.5 s. When
# setsid is available the watchdog kills the helper process group, which keeps
# shell stubs and helper descendants from surviving launch. A killed or failing
# probe counts as "not detected" and never blocks launch.
SESSION_BUS_PROBE_OUTPUT=""

session_bus_probe_command() {
    local output_file probe_pid kill_pid rc=1 self_pid use_process_group=0

    SESSION_BUS_PROBE_OUTPUT=""
    output_file="$(mktemp "${TMPDIR:-/tmp}/codex-session-bus-probe.XXXXXX")" || return 1
    self_pid=${BASHPID:-$$}

    if command -v setsid >/dev/null 2>&1; then
        setsid "$@" >"$output_file" 2>/dev/null &
        use_process_group=1
    else
        "$@" >"$output_file" 2>/dev/null &
    fi
    probe_pid=$!

    ( trap - EXIT
      sleep 0.5
      [ -r "/proc/$probe_pid/stat" ] || exit 0
      # /proc/<pid>/stat: "<pid> (comm) <state> <ppid> ..." — comm can contain
      # spaces/parens, so strip up to the last ") " before splitting fields.
      local stat_line rest
      stat_line=$(< "/proc/$probe_pid/stat") || exit 0
      rest=${stat_line##*) }
      set -- $rest
      # $1 = state, $2 = ppid
      [ "${2:-}" = "$self_pid" ] || exit 0
      if [ "$use_process_group" -eq 1 ]; then
          kill -TERM "-$probe_pid" 2>/dev/null || kill -TERM "$probe_pid" 2>/dev/null || true
          sleep 0.1
          kill -KILL "-$probe_pid" 2>/dev/null || kill -KILL "$probe_pid" 2>/dev/null || true
      else
          kill -TERM "$probe_pid" 2>/dev/null || true
          sleep 0.1
          kill -KILL "$probe_pid" 2>/dev/null || true
      fi
    ) 2>/dev/null &
    kill_pid=$!

    if wait "$probe_pid" 2>/dev/null; then
        rc=0
    else
        rc=$?
    fi
    kill "$kill_pid" 2>/dev/null || true
    wait "$kill_pid" 2>/dev/null || true

    if [ "$rc" -eq 0 ] && [ -r "$output_file" ]; then
        SESSION_BUS_PROBE_OUTPUT="$(< "$output_file")"
    fi
    rm -f "$output_file"
    return "$rc"
}

session_bus_assistive_technology_detected() {
    if command -v gsettings >/dev/null 2>&1; then
        if session_bus_probe_command gsettings get org.gnome.desktop.a11y.applications screen-reader-enabled; then
            case "$SESSION_BUS_PROBE_OUTPUT" in
                true) return 0 ;;
            esac
        fi
        if session_bus_probe_command gsettings get org.gnome.desktop.interface toolkit-accessibility; then
            case "$SESSION_BUS_PROBE_OUTPUT" in
                true) return 0 ;;
            esac
        fi
    fi

    if command -v busctl >/dev/null 2>&1; then
        if session_bus_probe_command busctl --user get-property org.a11y.Bus /org/a11y/bus org.a11y.Status IsEnabled; then
            case "$SESSION_BUS_PROBE_OUTPUT" in
                "b true") return 0 ;;
            esac
        fi
    fi

    return 1
}

should_force_renderer_accessibility() {
    local effective_platform

    if [ -n "${CODEX_FORCE_RENDERER_ACCESSIBILITY:-}" ]; then
        if truthy_env_value "$CODEX_FORCE_RENDERER_ACCESSIBILITY"; then
            return 0
        fi
        if falsey_env_value "$CODEX_FORCE_RENDERER_ACCESSIBILITY"; then
            return 1
        fi
        case "$CODEX_FORCE_RENDERER_ACCESSIBILITY" in
            auto|AUTO|Auto) ;;
            *)
                echo "Invalid CODEX_FORCE_RENDERER_ACCESSIBILITY='${CODEX_FORCE_RENDERER_ACCESSIBILITY:-}'; using rendering profile default" >&2
                ;;
        esac
    fi

    case "$ELECTRON_RENDERING_MODE" in
        wslg)
            return 1
            ;;
        wayland-gpu)
            effective_platform="$(effective_electron_ozone_platform)"
            [ "$effective_platform" = "wayland" ] && return 1
            ;;
    esac

    assistive_technology_detected
}

set_electron_defaults() {
    ELECTRON_OZONE_PLATFORM=""
    ELECTRON_OZONE_HINT="auto"
    ELECTRON_GPU_ENABLED=1
    ELECTRON_RENDERING_MODE="default"
    ELECTRON_WSLG_DETECTED=0
    ELECTRON_PLATFORM_EXPLICIT=0
    ELECTRON_OZONE_SWITCH_IN_ARGS=0
    ELECTRON_OZONE_PLATFORM_ARG=""
    ELECTRON_GL_SWITCH_PROVIDED=0
    ELECTRON_GL_SWITCH_ADDED=0
    ELECTRON_GPU_DISABLE_SWITCH_IN_ARGS=0
    ELECTRON_GPU_COMPOSITING_DISABLED=0
    ELECTRON_GPU_COMPOSITING_SWITCH_PROVIDED=0
    ELECTRON_DEV_SHM_USAGE_DISABLED=0
    ELECTRON_RENDERER_ACCESSIBILITY_FORCED=0
    ELECTRON_SCALE_SWITCH_IN_ARGS=0
    ELECTRON_FORCED_SCALE_FACTOR=""
    ELECTRON_ARGS=()
    ELECTRON_ENABLE_FEATURES=()
    local passthrough=0

    while [ "$#" -gt 0 ]; do
        if [ "$passthrough" -eq 1 ]; then
            case "$1" in
                --enable-features=*)
                    add_electron_enable_features "${1#--enable-features=}"
                    ;;
                *)
                    ELECTRON_ARGS+=("$1")
                    scan_electron_rendering_arg "$1"
                    ;;
            esac
            shift
            continue
        fi

        case "$1" in
            --)
                passthrough=1
                ;;
            --safe-mode)
                ELECTRON_OZONE_PLATFORM="x11"
                ELECTRON_OZONE_HINT=""
                ELECTRON_GPU_ENABLED=0
                ELECTRON_PLATFORM_EXPLICIT=1
                ;;
            --disable-gpu)
                ELECTRON_GPU_ENABLED=0
                ;;
            --enable-gpu)
                ELECTRON_GPU_ENABLED=1
                ;;
            --x11)
                ELECTRON_OZONE_PLATFORM="x11"
                ELECTRON_OZONE_HINT=""
                ELECTRON_PLATFORM_EXPLICIT=1
                ;;
            --wayland)
                ELECTRON_OZONE_PLATFORM="wayland"
                ELECTRON_OZONE_HINT=""
                ELECTRON_PLATFORM_EXPLICIT=1
                ;;
            --ozone-platform=*)
                ELECTRON_OZONE_PLATFORM="${1#--ozone-platform=}"
                ELECTRON_OZONE_HINT=""
                ELECTRON_PLATFORM_EXPLICIT=1
                ;;
            --ozone-platform-hint=*)
                ELECTRON_OZONE_HINT="${1#--ozone-platform-hint=}"
                ELECTRON_OZONE_PLATFORM=""
                ELECTRON_PLATFORM_EXPLICIT=1
                ;;
            --enable-features=*)
                add_electron_enable_features "${1#--enable-features=}"
                ;;
            *)
                ELECTRON_ARGS+=("$1")
                scan_electron_rendering_arg "$1"
                ;;
        esac
        shift
    done

    # ChromeOS / Crostini (Sommelier) auto-fallback to X11.
    #
    # On a Crostini Linux container the default Wayland session is provided by
    # Sommelier (see SOMMELIER_VERSION/SOMMELIER_VM_IDENTIFIER, exported by
    # cros-garcon for both terminal and app-menu launches). With the default
    # Electron path (--ozone-platform-hint=auto) Electron picks the Wayland
    # backend, reaches `window ready-to-show`, but the window never surfaces in
    # the ChromeOS shell — leaving the launcher icon spinning indefinitely with
    # no visible app. Forcing X11 (proxied by sommelier-x to ChromeOS) is the
    # documented workaround. Skip this only when the operator explicitly chose
    # a platform via --x11 / --wayland / --ozone-platform=*.
    #
    # See https://github.com/ilysenko/codex-desktop-linux/issues/95.
    if [ -z "$ELECTRON_OZONE_PLATFORM" ] \
        && [ "$ELECTRON_OZONE_HINT" = "auto" ] \
        && [ -n "${SOMMELIER_VERSION:-}" ]; then
        ELECTRON_OZONE_PLATFORM="x11"
        ELECTRON_OZONE_HINT=""
        echo "Detected Sommelier (SOMMELIER_VERSION=$SOMMELIER_VERSION); forcing --ozone-platform=x11 (Wayland windows are not visible under ChromeOS Crostini)"
    fi

    # Session-level operator overrides for HiDPI / fractional-scaling issues.
    # Explicit launcher/Electron flags still win over both env vars.
    apply_codex_ozone_platform_env
    if [ -n "${CODEX_FORCE_DEVICE_SCALE_FACTOR:-}" ]; then
        if [ "$ELECTRON_SCALE_SWITCH_IN_ARGS" -eq 1 ]; then
            echo "Ignoring CODEX_FORCE_DEVICE_SCALE_FACTOR; an explicit --force-device-scale-factor flag was provided"
        elif ELECTRON_FORCED_SCALE_FACTOR="$(codex_forced_device_scale_factor)"; then
            :
        else
            ELECTRON_FORCED_SCALE_FACTOR=""
        fi
    fi

    apply_electron_rendering_profile
}

load_feature_electron_args() {
    FEATURE_ELECTRON_ARGS=()
    [ -n "${FEATURE_ELECTRON_ARGS_DIR:-}" ] || return 0
    [ -d "$FEATURE_ELECTRON_ARGS_DIR" ] || return 0

    local args_file
    local line
    for args_file in "$FEATURE_ELECTRON_ARGS_DIR"/*; do
        [ -f "$args_file" ] || continue
        echo "Loading Linux feature Electron args: $args_file"
        while IFS= read -r line || [ -n "$line" ]; do
            line="${line%$'\r'}"
            case "$line" in
                ""|\#*) continue ;;
            esac
            FEATURE_ELECTRON_ARGS+=("$line")
        done < "$args_file"
    done
}

apply_feature_launcher_hook_env() {
    local assignment="$1"
    local name
    local value

    name="${assignment%%=*}"
    value="${assignment#*=}"
    if [ "$name" = "$assignment" ] || [[ ! "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
        echo "Ignoring invalid Linux feature launcher hook env assignment: $assignment"
        return 0
    fi

    printf -v "$name" '%s' "$value"
    export "$name"
}

# Remove entries from ELECTRON_ARGS whose switch name (the part before '=')
# matches one of the given switches, logging each drop against the new arg
# that supersedes it.
remove_superseded_electron_args() {
    local new_arg="$1"
    shift
    local -a kept=()
    local arg name switch dropped

    for arg in "${ELECTRON_ARGS[@]}"; do
        name="${arg%%=*}"
        dropped=0
        for switch in "$@"; do
            if [ "$name" = "$switch" ]; then
                echo "Dropping earlier Electron arg '$arg'; a launcher hook supplied '$new_arg'"
                dropped=1
                break
            fi
        done
        [ "$dropped" -eq 1 ] || kept+=("$arg")
    done

    ELECTRON_ARGS=("${kept[@]}")
}

apply_feature_launcher_hook_electron_arg() {
    local arg="$1"
    local switch

    case "$arg" in
        --enable-features=*)
            add_electron_enable_features "${arg#--enable-features=}"
            return 0
            ;;
    esac

    # Launcher hooks run last, after feature electron-args, the persistent
    # flags file, and pass-through CLI args have all been collected into
    # ELECTRON_ARGS. Chromium already resolves repeated switches last-wins, so
    # replace any earlier occurrence of the same switch instead of appending a
    # conflicting duplicate to the final argv. --ozone-platform and
    # --ozone-platform-hint are one family: an explicit platform supersedes an
    # earlier hint and vice versa.
    switch="${arg%%=*}"
    case "$switch" in
        --ozone-platform|--ozone-platform-hint)
            remove_superseded_electron_args "$arg" --ozone-platform --ozone-platform-hint
            ;;
        *)
            remove_superseded_electron_args "$arg" "$switch"
            ;;
    esac

    ELECTRON_ARGS+=("$arg")
    scan_electron_rendering_arg "$arg"
}

run_feature_launcher_hooks() {
    [ -n "${FEATURE_LAUNCHER_HOOK_DIR:-}" ] || return 0
    [ -d "$FEATURE_LAUNCHER_HOOK_DIR" ] || return 0

    local hook
    local output
    local status
    local line
    local payload

    for hook in "$FEATURE_LAUNCHER_HOOK_DIR"/*; do
        [ -f "$hook" ] || continue
        [ -x "$hook" ] || {
            echo "Skipping non-executable Linux feature launcher hook: $hook"
            continue
        }

        echo "Running Linux feature launcher hook: $hook"
        if output="$(
            CODEX_HOME="$CODEX_HOME" \
            CODEX_LINUX_APP_DIR="$SCRIPT_DIR" \
            CODEX_LINUX_APP_STATE_DIR="$APP_STATE_DIR" \
            CODEX_LINUX_FEATURES_DIR="$CODEX_LINUX_FEATURES_DIR" \
            CODEX_LINUX_LAUNCHER_LOG="$LOG_FILE" \
            CODEX_LINUX_FEATURE_HOOK_PHASE=launcher \
            codex_run_host_command "$hook" "${ELECTRON_ARGS[@]}"
        )"; then
            :
        else
            status=$?
            echo "WARN: Linux feature launcher hook failed with status $status: $hook"
            continue
        fi

        while IFS= read -r line || [ -n "$line" ]; do
            line="${line%$'\r'}"
            case "$line" in
                ""|\#*)
                    continue
                    ;;
                env\ *)
                    payload="${line#env }"
                    apply_feature_launcher_hook_env "$payload"
                    ;;
                electron-arg\ *)
                    payload="${line#electron-arg }"
                    apply_feature_launcher_hook_electron_arg "$payload"
                    ;;
                *)
                    echo "Ignoring unsupported Linux feature launcher hook output from $hook: $line"
                    ;;
            esac
        done <<< "$output"
    done
}

build_electron_launch_args() {
    local enable_features_arg

    # Feature launcher hooks (runtimeHooks.launcher) run after
    # set_electron_defaults() has already chosen the Ozone platform and device
    # scale factor -- whether from CODEX_OZONE_PLATFORM /
    # CODEX_FORCE_DEVICE_SCALE_FACTOR, the CODEX_LINUX_RENDERING_MODE profile
    # (wayland-gpu / wslg), or the Sommelier X11 fallback. If such a hook then
    # emits an explicit --ozone-platform / --force-device-scale-factor, both the
    # launcher-computed flag and the hook flag would land in the final Electron
    # argv (and a launcher-selected Wayland platform would still add
    # WaylandWindowDecorations below). Pass-through CLI args are scanned before
    # those defaults are computed and already suppress them; hook args are
    # scanned afterward, so drop the launcher-computed value here whenever any
    # explicit override reached the Electron args, so the hook flag wins cleanly.
    if [ -n "$ELECTRON_OZONE_PLATFORM" ] && [ "$ELECTRON_OZONE_SWITCH_IN_ARGS" -eq 1 ]; then
        echo "Dropping launcher-selected --ozone-platform=$ELECTRON_OZONE_PLATFORM; an explicit --ozone-platform arg overrides it"
        ELECTRON_OZONE_PLATFORM=""
        ELECTRON_OZONE_HINT=""
    fi
    if [ -n "$ELECTRON_FORCED_SCALE_FACTOR" ] && [ "$ELECTRON_SCALE_SWITCH_IN_ARGS" -eq 1 ]; then
        echo "Dropping launcher-selected --force-device-scale-factor=$ELECTRON_FORCED_SCALE_FACTOR; an explicit --force-device-scale-factor arg overrides it"
        ELECTRON_FORCED_SCALE_FACTOR=""
    fi

    ELECTRON_LAUNCH_ARGS=(
        --no-sandbox
        --class="$CODEX_LINUX_APP_ID"
        --app-id="$CODEX_LINUX_APP_ID"
        --disable-gpu-sandbox
    )

    if should_disable_dev_shm_usage; then
        ELECTRON_LAUNCH_ARGS+=(--disable-dev-shm-usage)
        ELECTRON_DEV_SHM_USAGE_DISABLED=1
    else
        ELECTRON_DEV_SHM_USAGE_DISABLED=0
    fi

    if should_disable_gpu_compositing; then
        ELECTRON_LAUNCH_ARGS+=(--disable-gpu-compositing)
        ELECTRON_GPU_COMPOSITING_DISABLED=1
    else
        ELECTRON_GPU_COMPOSITING_DISABLED=0
    fi

    if [ "$CODEX_LINUX_APP_ID" != "codex-desktop" ]; then
        ELECTRON_LAUNCH_ARGS+=(--user-data-dir="${CODEX_ELECTRON_USER_DATA_DIR:-$APP_STATE_DIR/electron-user-data}")
    elif [ -n "${CODEX_ELECTRON_USER_DATA_DIR:-}" ]; then
        ELECTRON_LAUNCH_ARGS+=(--user-data-dir="$CODEX_ELECTRON_USER_DATA_DIR")
    fi

    if [ -n "$ELECTRON_OZONE_PLATFORM" ]; then
        ELECTRON_LAUNCH_ARGS+=(--ozone-platform="$ELECTRON_OZONE_PLATFORM")
    elif [ -n "$ELECTRON_OZONE_HINT" ] && [ "$ELECTRON_OZONE_SWITCH_IN_ARGS" -eq 0 ]; then
        ELECTRON_LAUNCH_ARGS+=(--ozone-platform-hint="$ELECTRON_OZONE_HINT")
    fi

    if [ -n "$ELECTRON_FORCED_SCALE_FACTOR" ]; then
        ELECTRON_LAUNCH_ARGS+=(--force-device-scale-factor="$ELECTRON_FORCED_SCALE_FACTOR")
    fi

    if [ "$ELECTRON_GPU_ENABLED" != "1" ]; then
        ELECTRON_LAUNCH_ARGS+=(--disable-gpu --disable-features=Vulkan)
    fi

    if should_force_renderer_accessibility; then
        ELECTRON_LAUNCH_ARGS+=(--force-renderer-accessibility)
        ELECTRON_RENDERER_ACCESSIBILITY_FORCED=1
    else
        ELECTRON_RENDERER_ACCESSIBILITY_FORCED=0
    fi

    if [ "$ELECTRON_OZONE_PLATFORM" = "wayland" ]; then
        add_electron_enable_features "WaylandWindowDecorations"
    fi

    if enable_features_arg="$(electron_enable_features_arg)"; then
        ELECTRON_LAUNCH_ARGS+=("$enable_features_arg")
    fi

}

print_scaling_diagnostics() {
    local key arg

    echo "$CODEX_LINUX_APP_DISPLAY_NAME scaling diagnostics"
    echo ""
    echo "Session environment:"
    for key in XDG_SESSION_TYPE XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP WAYLAND_DISPLAY DISPLAY \
        GDK_BACKEND GDK_SCALE GDK_DPI_SCALE QT_SCALE_FACTOR QT_AUTO_SCREEN_SCALE_FACTOR \
        ELECTRON_OZONE_PLATFORM_HINT CODEX_OZONE_PLATFORM CODEX_FORCE_DEVICE_SCALE_FACTOR \
        CODEX_LINUX_RENDERING_MODE; do
        printf '  %s=%s\n' "$key" "${!key:-<unset>}"
    done

    echo ""
    echo "GNOME settings:"
    if command -v gsettings >/dev/null 2>&1; then
        printf '  org.gnome.desktop.interface scaling-factor: %s\n' \
            "$(gsettings get org.gnome.desktop.interface scaling-factor 2>/dev/null || echo unavailable)"
        printf '  org.gnome.desktop.interface text-scaling-factor: %s\n' \
            "$(gsettings get org.gnome.desktop.interface text-scaling-factor 2>/dev/null || echo unavailable)"
        printf '  org.gnome.mutter experimental-features: %s\n' \
            "$(gsettings get org.gnome.mutter experimental-features 2>/dev/null || echo unavailable)"
    else
        echo "  gsettings not available"
    fi

    echo ""
    echo "X resources (on X11/XWayland Chromium derives its scale from Xft.dpi / 96, GDK_SCALE, and GDK_DPI_SCALE):"
    if [ -n "${DISPLAY:-}" ] && command -v xrdb >/dev/null 2>&1; then
        xrdb -query 2>/dev/null | grep -i 'Xft.dpi' | sed 's/^/  /' \
            || echo "  Xft.dpi not set (Chromium assumes 96 dpi = scale 1)"
    else
        echo "  unavailable (no DISPLAY or xrdb missing)"
    fi

    echo ""
    echo "Monitors:"
    if [ -n "${DISPLAY:-}" ] && command -v xrandr >/dev/null 2>&1; then
        xrandr --listmonitors 2>/dev/null | sed 's/^/  /' || echo "  xrandr query failed"
    else
        echo "  xrandr unavailable"
    fi

    echo ""
    echo "Electron runtime:"
    printf '  electron: %s\n' "$(cat "$SCRIPT_DIR/version" 2>/dev/null || echo unknown)"
    printf '  binary: %s\n' "$SCRIPT_DIR/electron"

    echo ""
    echo "Computed Electron launch flags (feature launcher hooks are not executed here):"
    load_feature_electron_args
    load_user_electron_flags
    set_electron_defaults "${FEATURE_ELECTRON_ARGS[@]}" "${USER_ELECTRON_FLAGS[@]}" "${LAUNCHER_ARGS[@]:1}"
    build_electron_launch_args
    for arg in "${ELECTRON_LAUNCH_ARGS[@]}" "${ELECTRON_ARGS[@]}"; do
        printf '  %s\n' "$arg"
    done

    echo ""
    echo "If the UI is oversized or blurry, try:"
    echo "  CODEX_FORCE_DEVICE_SCALE_FACTOR=1 <launch command>"
    echo "  CODEX_OZONE_PLATFORM=x11 <launch command>"
    echo "  CODEX_OZONE_PLATFORM=wayland <launch command>"
    echo "See docs/troubleshooting.md (Oversized or blurry UI) for details."
}

configure_side_by_side_app_env() {
    if [ "$CODEX_LINUX_APP_ID" = "codex-desktop" ]; then
        return 0
    fi

    XDG_CONFIG_HOME="${CODEX_XDG_CONFIG_HOME:-$APP_STATE_DIR/xdg-config}"
    CODEX_ELECTRON_USER_DATA_DIR="${CODEX_ELECTRON_USER_DATA_DIR:-$APP_STATE_DIR/electron-user-data}"
    export XDG_CONFIG_HOME CODEX_ELECTRON_USER_DATA_DIR
}

cleanup_launcher() {
    if [ -n "${ELECTRON_PID:-}" ] && [ -f "$APP_PID_FILE" ]; then
        local current_pid
        current_pid="$(cat "$APP_PID_FILE" 2>/dev/null || true)"
        if [ "$current_pid" = "$ELECTRON_PID" ]; then
            rm -f "$APP_PID_FILE"
        fi
    fi

    if [ -n "${STARTED_WEBVIEW_PID:-}" ] && pid_is_webview_server "$STARTED_WEBVIEW_PID"; then
        kill "$STARTED_WEBVIEW_PID" 2>/dev/null || true
        rm -f "$WEBVIEW_PID_FILE"
    fi
}

launcher_lock_wait_seconds() {
    local value="${CODEX_LAUNCHER_LOCK_WAIT_SECONDS:-5}"

    case "$value" in
        ""|*[!0-9]*)
            echo "WARN: invalid CODEX_LAUNCHER_LOCK_WAIT_SECONDS='${CODEX_LAUNCHER_LOCK_WAIT_SECONDS:-}'; using 5" >&2
            echo 5
            return 0
            ;;
    esac

    if [ "$value" -gt 300 ]; then
        echo "WARN: CODEX_LAUNCHER_LOCK_WAIT_SECONDS=$value is too high; using 300" >&2
        echo 300
        return 0
    fi

    echo "$value"
}

# Serialize the running-app detection -> Electron spawn -> app.pid write
# critical section across concurrent launchers. Without this, two launches
# racing through detection can both cold-start, and the loser's cleanup later
# deletes the survivor's app.pid marker, making it undetectable. A dedicated
# parent-death-bound helper owns the flock so no launcher child can inherit it.
acquire_launcher_lock() {
    local wait_seconds
    local status=""
    local attempts

    [ "$LAUNCHER_LOCK_HELD" -eq 0 ] || return 0
    LAUNCHER_LOCK_TIMED_OUT=0
    wait_seconds="$(launcher_lock_wait_seconds)"
    LAUNCHER_LOCK_STATUS_PATH="$APP_STATE_DIR/.launcher.lock.status.$$"
    rm -f "$LAUNCHER_LOCK_STATUS_PATH"

    # A dedicated helper owns the flock, so arbitrary launcher children never
    # inherit the locked file description. PR_SET_PDEATHSIG releases it even
    # when the launcher is SIGKILLed while a hook or cache worker survives.
    ( trap - EXIT
      exec python3 - \
          "$$" \
          "$APP_STATE_DIR/launcher.lock" \
          "$wait_seconds" \
          "$LAUNCHER_LOCK_STATUS_PATH" <<'PY'
import ctypes
import ctypes.util
import fcntl
import os
import signal
import sys
import time

parent_pid = int(sys.argv[1])
lock_path, wait_text, status_path = sys.argv[2:]
wait_seconds = int(wait_text)

libc_name = ctypes.util.find_library("c") or "libc.so.6"
try:
    libc = ctypes.CDLL(libc_name, use_errno=True)
    if libc.prctl(1, signal.SIGTERM, 0, 0, 0) != 0:
        sys.exit(1)
except OSError:
    sys.exit(1)
if os.getppid() != parent_pid:
    sys.exit(1)

def release_lock(_signum, _frame):
    raise SystemExit(0)


signal.signal(signal.SIGTERM, release_lock)
signal.signal(signal.SIGUSR1, release_lock)
lock_fd = os.open(lock_path, os.O_CREAT | os.O_WRONLY, 0o600)
try:
    deadline = time.monotonic() + wait_seconds
    locked = False
    while True:
        try:
            fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            locked = True
            break
        except BlockingIOError:
            if time.monotonic() >= deadline:
                break
            time.sleep(0.05)

    with open(status_path, "w", encoding="utf-8") as status_file:
        status_file.write("locked\n" if locked else "timeout\n")

    if locked:
        while True:
            signal.pause()
finally:
    os.close(lock_fd)
    try:
        os.unlink(status_path)
    except FileNotFoundError:
        pass
PY
    ) >/dev/null 2>&1 &
    LAUNCHER_LOCK_HELPER_PID=$!
    LAUNCHER_LOCK_HELPER_START_TIME="$(pid_start_time "$LAUNCHER_LOCK_HELPER_PID" 2>/dev/null || true)"
    if [ -z "$LAUNCHER_LOCK_HELPER_START_TIME" ]; then
        LAUNCHER_LOCK_TIMED_OUT=1
        return 0
    fi

    attempts=$((wait_seconds * 20 + 20))
    for _ in $(seq 1 "$attempts"); do
        if [ -s "$LAUNCHER_LOCK_STATUS_PATH" ]; then
            status="$(cat "$LAUNCHER_LOCK_STATUS_PATH" 2>/dev/null || true)"
            break
        fi
        kill -0 "$LAUNCHER_LOCK_HELPER_PID" 2>/dev/null || break
        sleep 0.05
    done

    if [ "$status" != "locked" ]; then
        LAUNCHER_LOCK_TIMED_OUT=1
        if stop_launcher_lock_helper; then
            wait "$LAUNCHER_LOCK_HELPER_PID" 2>/dev/null || true
        fi
        LAUNCHER_LOCK_HELPER_PID=""
        LAUNCHER_LOCK_HELPER_START_TIME=""
        rm -f "$LAUNCHER_LOCK_STATUS_PATH"
        launcher_notice "WARN: timed out waiting for launcher.lock after ${wait_seconds}s; refusing to start another app process."
        return 0
    fi

    LAUNCHER_LOCK_HELD=1
    if ! launcher_lock_helper_is_active; then
        LAUNCHER_LOCK_HELD=0
        if stop_launcher_lock_helper; then
            wait "$LAUNCHER_LOCK_HELPER_PID" 2>/dev/null || true
        fi
        LAUNCHER_LOCK_HELPER_PID=""
        LAUNCHER_LOCK_HELPER_START_TIME=""
        LAUNCHER_LOCK_TIMED_OUT=1
        rm -f "$LAUNCHER_LOCK_STATUS_PATH"
        return 0
    fi
    rm -f "$LAUNCHER_LOCK_STATUS_PATH"
}

launcher_lock_helper_is_active() {
    local actual_start_time
    local cmdline

    [ "$LAUNCHER_LOCK_HELD" -eq 1 ] || return 1
    [[ "$LAUNCHER_LOCK_HELPER_PID" =~ ^[0-9]+$ ]] || return 1
    actual_start_time="$(pid_start_time "$LAUNCHER_LOCK_HELPER_PID" 2>/dev/null || true)"
    [ -n "$actual_start_time" ] && [ "$actual_start_time" = "$LAUNCHER_LOCK_HELPER_START_TIME" ] || return 1
    pid_parent_matches "$LAUNCHER_LOCK_HELPER_PID" "${BASHPID:-$$}" || return 1
    cmdline="$(pid_cmdline "$LAUNCHER_LOCK_HELPER_PID")"
    [[ "$cmdline" == *" $APP_STATE_DIR/launcher.lock "*" $LAUNCHER_LOCK_STATUS_PATH"* ]]
}

stop_launcher_lock_helper() {
    local actual_start_time
    local cmdline

    [[ "$LAUNCHER_LOCK_HELPER_PID" =~ ^[0-9]+$ ]] || return 1
    actual_start_time="$(pid_start_time "$LAUNCHER_LOCK_HELPER_PID" 2>/dev/null || true)"
    [ -n "$actual_start_time" ] && [ "$actual_start_time" = "$LAUNCHER_LOCK_HELPER_START_TIME" ] || return 1
    pid_parent_matches "$LAUNCHER_LOCK_HELPER_PID" "${BASHPID:-$$}" || return 1
    cmdline="$(pid_cmdline "$LAUNCHER_LOCK_HELPER_PID")"
    [[ "$cmdline" == *" $APP_STATE_DIR/launcher.lock "*" $LAUNCHER_LOCK_STATUS_PATH"* ]] || return 1
    kill -TERM "$LAUNCHER_LOCK_HELPER_PID" 2>/dev/null || ! kill -0 "$LAUNCHER_LOCK_HELPER_PID" 2>/dev/null
}

release_launcher_lock() {
    [ "$LAUNCHER_LOCK_HELD" -eq 1 ] || return 0
    LAUNCHER_LOCK_HELD=0
    if [ -n "$LAUNCHER_LOCK_HELPER_PID" ]; then
        if stop_launcher_lock_helper; then
            wait "$LAUNCHER_LOCK_HELPER_PID" 2>/dev/null || true
        else
            echo "WARN: launcher lock helper did not exit after a verified release signal" >&2
        fi
    fi
    LAUNCHER_LOCK_HELPER_PID=""
    LAUNCHER_LOCK_HELPER_START_TIME=""
    rm -f "$LAUNCHER_LOCK_STATUS_PATH"
}

refresh_launch_state() {
    WARM_START=0
    RUNNING_APP_PID=""
    detect_warm_start
    reconcile_runtime_state
}

refresh_launch_state_quick() {
    local pid

    WARM_START=0
    RUNNING_APP_PID=""
    if pid="$(find_running_app_pid)"; then
        set_detected_running_app "$pid"
    fi
}

prepare_launch_state_under_lock() {
    running_app_is_active && return 0

    acquire_launcher_lock
    if [ "$LAUNCHER_LOCK_HELD" -ne 1 ] || [ "$LAUNCHER_LOCK_TIMED_OUT" -eq 1 ]; then
        release_launcher_lock
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME could not acquire launcher state within the configured timeout. No new app process was started; retry after the current launch finishes."
        exit 1
    fi

    log_phase "launcher_lock_ready"
    refresh_launch_state_quick
    log_phase "launch_state_refreshed_under_lock"
    recover_unhealthy_running_app

    # A healthy app discovered while waiting owns its webview already. Cold
    # starts keep the lock through webview readiness and app.pid publication.
    if running_app_is_active; then
        release_launcher_lock
    fi
}

require_active_launcher_lock() {
    [ "$LAUNCHER_LOCK_HELD" -eq 1 ] || return 0
    if ! launcher_lock_helper_is_active; then
        notify_error "$CODEX_LINUX_APP_DISPLAY_NAME lost launcher serialization before Electron startup. No app process was started; retry the launch."
        exit 1
    fi
}

launch_electron() {
    cd "$SCRIPT_DIR"
    log_phase "electron_launch"

    ensure_user_electron_flags_file
    load_feature_electron_args
    load_user_electron_flags
    set_electron_defaults "${FEATURE_ELECTRON_ARGS[@]}" "${USER_ELECTRON_FLAGS[@]}" "$@"
    run_feature_launcher_hooks
    build_electron_launch_args

    if [ "$WARM_START" -eq 1 ]; then
        release_launcher_lock
        echo "Electron warm-start handoff: pid=$RUNNING_APP_PID rendering_mode=$ELECTRON_RENDERING_MODE wslg_detected=$ELECTRON_WSLG_DETECTED ozone_platform=${ELECTRON_OZONE_PLATFORM:-default} ozone_hint=${ELECTRON_OZONE_HINT:-none} gpu_enabled=$ELECTRON_GPU_ENABLED gpu_disable_arg=$ELECTRON_GPU_DISABLE_SWITCH_IN_ARGS gpu_compositing_disabled=$ELECTRON_GPU_COMPOSITING_DISABLED dev_shm_usage_disabled=$ELECTRON_DEV_SHM_USAGE_DISABLED gl_switch_added=$ELECTRON_GL_SWITCH_ADDED renderer_accessibility_forced=$ELECTRON_RENDERER_ACCESSIBILITY_FORCED forced_scale=${ELECTRON_FORCED_SCALE_FACTOR:-none}"
        if [ "$ELECTRON_GPU_COMPOSITING_DISABLED" = "0" ] && [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
            echo "Rendering hint: if the side panel flickers or appears transparent on Wayland, retry with CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=1"
        fi
        unset ELECTRON_RUN_AS_NODE
        "$SCRIPT_DIR/electron" "${ELECTRON_LAUNCH_ARGS[@]}" "${ELECTRON_ARGS[@]}"
        return $?
    fi

    echo "Electron launch mode: rendering_mode=$ELECTRON_RENDERING_MODE wslg_detected=$ELECTRON_WSLG_DETECTED ozone_platform=${ELECTRON_OZONE_PLATFORM:-default} ozone_hint=${ELECTRON_OZONE_HINT:-none} gpu_enabled=$ELECTRON_GPU_ENABLED gpu_disable_arg=$ELECTRON_GPU_DISABLE_SWITCH_IN_ARGS gpu_compositing_disabled=$ELECTRON_GPU_COMPOSITING_DISABLED dev_shm_usage_disabled=$ELECTRON_DEV_SHM_USAGE_DISABLED gl_switch_added=$ELECTRON_GL_SWITCH_ADDED renderer_accessibility_forced=$ELECTRON_RENDERER_ACCESSIBILITY_FORCED forced_scale=${ELECTRON_FORCED_SCALE_FACTOR:-none}"
    if [ "$ELECTRON_GPU_COMPOSITING_DISABLED" = "0" ] && [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
        echo "Rendering hint: if the side panel flickers or appears transparent on Wayland, retry with CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=1"
    fi
    require_active_launcher_lock
    (
        unset ELECTRON_RUN_AS_NODE
        exec "$SCRIPT_DIR/electron" "${ELECTRON_LAUNCH_ARGS[@]}" "${ELECTRON_ARGS[@]}"
    ) &
    ELECTRON_PID=$!
    if [ -n "${RUNNING_APP_PID:-}" ] && pid_matches_executable "$RUNNING_APP_PID" "$SCRIPT_DIR/electron"; then
        echo "Preserving ChatGPT Desktop pid=$RUNNING_APP_PID liveness marker for second-instance handoff"
    else
        echo "$ELECTRON_PID" > "$APP_PID_FILE"
    fi
    release_launcher_lock
    log_phase "electron_spawned"

    set +e
    wait "$ELECTRON_PID"
    local status=$?
    set -e
    run_feature_after_exit_hooks "$status"
    return "$status"
}

hydrate_graphical_session_env
configure_side_by_side_app_env
export_feature_runtime_env
load_packaged_runtime_helper
prepend_managed_node_runtime_to_path
source_feature_env_files
if [ "$LAUNCHER_DIAGNOSE_SCALING" -eq 1 ]; then
    print_scaling_diagnostics
    exit 0
fi
register_url_scheme_handlers
log_phase "initial_launch_state_refresh_start"
refresh_launch_state
log_phase "initial_launch_state_refreshed"
trap cleanup_launcher EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP

recover_unhealthy_running_app
prepare_launch_state_under_lock

if send_warm_start_launch_action "${LAUNCHER_ARGS[@]}"; then
    echo "Sent launch args over warm-start IPC"
    log_phase "warm_start_ipc_sent"
    exit 0
elif [ "$WARM_START" -eq 1 ]; then
    echo "Warm-start IPC unavailable; falling back to Electron second-instance handoff"
fi

if using_second_instance_handoff; then
    echo "Detected running ChatGPT Desktop pid=$RUNNING_APP_PID; using Electron second-instance handoff"
    log_phase "second_instance_handoff_ready"
elif needs_cold_start; then
    log_phase "feature_prelaunch_start"
    run_feature_prelaunch_hooks
    log_phase "feature_prelaunch"
    log_phase "packaged_prelaunch_start"
    run_packaged_runtime_prelaunch
    log_phase "packaged_prelaunch"
    start_webview_server
else
    echo "Skipping packaged prelaunch and webview setup for warm start"
    log_phase "warm_start_ready"
fi

if [ -z "${CODEX_CLI_PATH:-}" ]; then
    CODEX_CLI_PATH="$(find_codex_cli || true)"
    export CODEX_CLI_PATH
    log_phase "cli_lookup"
fi
export CHROME_DESKTOP="${CHROME_DESKTOP:-$CODEX_LINUX_APP_ID.desktop}"
if ! truthy_env_value "${CODEX_LINUX_ALLOW_RENDERER_URL_OVERRIDE:-}"; then
    if [ -n "${ELECTRON_RENDERER_URL:-}" ] && [ "$ELECTRON_RENDERER_URL" != "$WEBVIEW_ORIGIN/" ]; then
        echo "Ignoring inherited ELECTRON_RENDERER_URL; set CODEX_LINUX_ALLOW_RENDERER_URL_OVERRIDE=1 to allow overrides"
    fi
    export ELECTRON_RENDERER_URL="$WEBVIEW_ORIGIN/"
else
    export ELECTRON_RENDERER_URL="${ELECTRON_RENDERER_URL:-$WEBVIEW_ORIGIN/}"
fi

if needs_cold_start && [ -z "$CODEX_CLI_PATH" ]; then
    if is_interactive_terminal; then
        if prompt_install_missing_cli; then
            if ! run_cli_preflight 1; then
                notify_error "Codex CLI installation was attempted but did not succeed. Retry by reopening the app, or install into a fresh safe prefix: (umask 0022; npm i -g --include=optional --prefix ~/.codex-cli-npm @openai/codex)"
                exit 1
            fi
        fi
    elif ! run_gui_cli_prompt; then
        notify_error "Codex CLI is required but is not installed. Reopen the app to retry the automatic install flow, or install into a fresh safe prefix: (umask 0022; npm i -g --include=optional --prefix ~/.codex-cli-npm @openai/codex)"
        exit 1
    fi
fi

if needs_cold_start && [ -z "$CODEX_CLI_PATH" ]; then
    notify_error "Codex CLI is required but is not installed. Reopen the app to retry the automatic install flow, or install into a fresh safe prefix: (umask 0022; npm i -g --include=optional --prefix ~/.codex-cli-npm @openai/codex)"
    exit 1
fi

if [ -n "$CODEX_CLI_PATH" ]; then
    if ! verify_cli_launch_path; then
        notify_error "The selected Codex CLI path does not resolve to an executable file. Reinstall the CLI or set CODEX_CLI_PATH to a working executable."
        exit 1
    fi
    log_phase "cli_launch_path_verified"
fi

if needs_cold_start; then
    cli_repair_required=0
    if codex_cli_missing_optional_dependency "$CODEX_CLI_PATH"; then
        cli_repair_required=1
    fi
    if [ "${CODEX_SYNC_CLI_PREFLIGHT:-0}" = "1" ]; then
        if ! run_cli_preflight 0 "$cli_repair_required"; then
            notify_error "The selected Codex CLI is missing its Linux optional dependency and could not be repaired automatically. Reinstall it with: npm i -g --include=optional @openai/codex@latest or npm i -g --include=optional --prefix ~/.local @openai/codex@latest"
            exit 1
        fi
        if [ "$cli_repair_required" = "1" ]; then
            log_phase "cli_preflight_repair_sync"
        else
            log_phase "cli_preflight_sync"
        fi
    elif [ "$cli_repair_required" = "1" ]; then
        if ! run_cli_preflight 0 1; then
            notify_error "The selected Codex CLI is missing its Linux optional dependency and could not be repaired automatically. Reinstall it with: npm i -g --include=optional @openai/codex@latest or npm i -g --include=optional --prefix ~/.local @openai/codex@latest"
            exit 1
        fi
        log_phase "cli_preflight_repair_sync"
    else
        run_cli_preflight_background
        log_phase "cli_preflight_backgrounded"
    fi
fi

export_packaged_runtime_env
if needs_cold_start; then
    log_phase "cold_start_cache_sync_start"
    clear_bundled_marketplace_tmp_cache
    log_phase "bundled_marketplace_tmp_cleared"
    stage_bundled_marketplace_metadata
    log_phase "bundled_marketplace_metadata_staged"
    sync_browser_use_bundled_plugin_cache &
    SYNC_BROWSER_USE_PID=$!
    sync_chrome_bundled_plugin_cache &
    SYNC_CHROME_PID=$!
    sync_computer_use_bundled_plugin_cache &
    SYNC_COMPUTER_USE_PID=$!
    sync_read_aloud_bundled_plugin_cache &
    SYNC_READ_ALOUD_PID=$!
    sync_extra_bundled_plugin_cache &
    SYNC_EXTRA_PID=$!
    wait "$SYNC_BROWSER_USE_PID"
    log_phase "browser_use_plugin_cache_synced"
    wait "$SYNC_CHROME_PID"
    log_phase "chrome_plugin_cache_synced"
    wait "$SYNC_COMPUTER_USE_PID"
    log_phase "computer_use_plugin_cache_synced"
    wait "$SYNC_READ_ALOUD_PID"
    log_phase "read_aloud_plugin_cache_synced"
    wait "$SYNC_EXTRA_PID"
    log_phase "extra_bundled_plugin_cache_synced"
    run_cold_start_hooks
    log_phase "cold_start_hooks_dispatched"
    await_webview_server_ready
fi
resolve_browser_use_runtime_env
log_phase "browser_use_runtime_env_resolved"

log_codex_cli_path
echo "Using managed Node.js runtime=${CODEX_MANAGED_NODE_RUNTIME_DIR:-$SCRIPT_DIR/resources/node-runtime}"

launch_electron "${LAUNCHER_ARGS[@]}"
