lib/openagents/application.ex

58e6347eeb72 · 5 KB

defmodule OpenAgents.Application do
  # See https://elixir.hexdocs.pm/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  require Logger

  @impl true
  def start(_type, _args) do
    case Application.get_env(:openagents, :runtime_role, :web) do
      :scv -> start_scv_worker()
      :web -> start_web()
    end
  end

  defp start_web do
    runtime_config = OpenAgents.RuntimeConfig.install!()

    # Releases migrate on boot (RELEASE-001): the schema must precede traffic.
    # Ecto.Migrator.with_repo takes the migration advisory lock, so concurrent
    # fleet nodes serialize safely and an already-migrated DB is a no-op.
    if Application.get_env(:openagents, :migrate_on_boot, false) do
      OpenAgents.Release.migrate()
    end

    # Install immutable release artifacts before any traffic can reach them.
    source_manifest = OpenAgents.Persona.SourceManifest.load!()
    _persona = OpenAgents.Persona.install!(source_manifest)
    _program_catalog = OpenAgents.ProgramArtifacts.install!()
    :ok = OpenAgents.Voice.Config.validate_boot!()

    unless OpenAgents.ContentVault.configured?() do
      Logger.warning(
        "OpenAgents.ContentVault is not configured: :content_encryption_key is missing or not a 32-byte base64-encoded key. " <>
          "Writing sealed transcript, project note, observation, or session summary content will fail."
      )
    end

    # Build and install the tool catalog; this snapshot is passed to the
    # embedding warmer and the turn supervisor below.
    tool_modules =
      if OpenAgents.RuntimeConfig.feature_enabled?(runtime_config, :tools) do
        Application.fetch_env!(:openagents, :tools)
      else
        []
      end

    tool_snapshot = OpenAgents.Tools.Registry.install!(tool_modules)
    :ok = OpenAgents.RuntimeConfig.verify_startup!(runtime_config, tool_snapshot)

    children =
      [
        OpenAgentsWeb.Telemetry,
        OpenAgents.Repo,
        OpenAgents.ReleaseState,
        OpenAgents.Forge.CacheReadiness,
        # Deployment identity and boot convergence must settle before cluster
        # discovery or the endpoint can make this node externally reachable.
        OpenAgents.Forge.DeploymentNode,
        OpenAgents.Forge.BootConverge,
        {DNSCluster, query: Application.get_env(:openagents, :dns_cluster_query) || :ignore},
        {Phoenix.PubSub, name: OpenAgents.PubSub},
        OpenAgents.Inference.Health,
        OpenAgents.RuntimeSupervisor,
        OpenAgentsWeb.BoxRateLimiter,
        {Registry, keys: :unique, name: OpenAgents.BoxRunRegistry},
        OpenAgents.BoxRunSupervisor,
        OpenAgents.BoxRunRecovery,
        OpenAgents.Box.Reconciler,
        OpenAgents.Forge.AssignmentExpiry,
        OpenAgents.Forge.AssignmentCredentialVault,
        OpenAgents.Machines.PairingExpiry
      ] ++ analytics_children() ++ [OpenAgentsWeb.Endpoint]

    # See https://elixir.hexdocs.pm/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: OpenAgents.Supervisor]
    result = Supervisor.start_link(children, opts)

    # Warm the tool-embedding index off the boot path: the HTTP client is up by
    # now, and if embeddings are disabled this is an immediate no-op. Tool
    # selection falls back to lexical until (and if) the index is warm, so a
    # cold or failed warm never blocks or breaks a turn.
    #
    # Changelog backfill is likewise idempotent and non-blocking.
    case result do
      {:ok, _pid} ->
        Task.start(fn -> OpenAgents.Tools.Embeddings.warm(tool_snapshot) end)
        Task.start(fn -> OpenAgents.Changelog.Backfill.boot() end)

      _error ->
        :ok
    end

    result
  end

  defp start_scv_worker do
    child = %{
      id: OpenAgents.SCV.Worker,
      start: {Task, :start_link, [fn -> run_scv_worker() end]},
      restart: :temporary
    }

    Supervisor.start_link([child], strategy: :one_for_one, name: OpenAgents.SCV.Supervisor)
  end

  # The analytics supervision tree starts only when a project token was
  # configured at boot; without one, OpenAgents.Analytics is a no-op and no
  # PostHog process exists. See docs/2026-08-21-posthog-integration-runbook.md.
  defp analytics_children do
    case posthog_config() do
      nil -> []
      config -> [{PostHog.Supervisor, config}]
    end
  end

  @doc false
  def posthog_config do
    case Application.get_env(:posthog, :api_key) do
      key when is_binary(key) and key != "" ->
        :posthog
        |> Application.get_all_env()
        |> Keyword.drop([:enable, :enable_error_tracking])
        |> PostHog.Config.validate!()

      _missing ->
        nil
    end
  end

  defp run_scv_worker do
    exit_status =
      try do
        OpenAgents.SCV.Worker.run_from_env!()
        0
      rescue
        _error -> 1
      catch
        _kind, _reason -> 1
      end

    System.stop(exit_status)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  @impl true
  def config_change(changed, _new, removed) do
    OpenAgentsWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end