lib/openagents/scv/execution.ex

main at 58e6347eeb72 · 3 KB

defmodule OpenAgents.SCV.Execution do
  @moduledoc "Durable authority and terminal receipt for one Codex-backed SCV run."

  use Ecto.Schema
  import Ecto.Changeset

  alias OpenAgents.SCV.DriverAccount
  alias OpenAgents.SCV.ExecutionEvent

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  @timestamps_opts [type: :utc_datetime_usec]
  @terminal_statuses ~w(succeeded failed cancelled uncertain)

  schema "scv_runs" do
    belongs_to :driver_account, DriverAccount
    field :driver, :string, default: "codex_app_server"
    field :principal, :string
    field :repository_revision, :string
    field :objective, :string, redact: true
    field :permission_profile, :string, default: "read_only"
    field :model, :string, default: "gpt-5.6-luna"
    field :reasoning_effort, :string, default: "low"
    field :status, :string, default: "running"
    field :owner_node, :string
    field :generation, :integer
    field :lease_expires_at, :utc_datetime_usec
    field :driver_thread_id, :string
    field :driver_turn_id, :string
    field :report, :string, redact: true
    field :report_digest, :string
    field :event_count, :integer, default: 0
    field :usage, :map
    field :resources, :map
    field :error_code, :string
    field :started_at, :utc_datetime_usec
    field :completed_at, :utc_datetime_usec
    has_many :events, ExecutionEvent, foreign_key: :run_id
    timestamps()
  end

  @type t :: %__MODULE__{}

  def terminal?(%__MODULE__{status: status}), do: status in @terminal_statuses

  @doc false
  def claim_changeset(execution, attributes) do
    execution
    |> cast(attributes, [
      :id,
      :driver_account_id,
      :principal,
      :repository_revision,
      :objective,
      :reasoning_effort,
      :owner_node,
      :generation,
      :lease_expires_at,
      :started_at
    ])
    |> validate_required([
      :driver_account_id,
      :principal,
      :repository_revision,
      :objective,
      :owner_node,
      :generation,
      :lease_expires_at,
      :started_at
    ])
    |> validate_length(:objective, min: 1, max: 32_768, count: :bytes)
    |> validate_format(:repository_revision, ~r/\A[0-9a-f]{40}\z/)
    |> validate_inclusion(:reasoning_effort, ~w(none low))
    |> put_change(:driver, "codex_app_server")
    |> put_change(:permission_profile, "read_only")
    |> put_change(:model, "gpt-5.6-luna")
    |> put_change(:status, "running")
    |> foreign_key_constraint(:driver_account_id)
    |> unique_constraint(:driver_account_id, name: :scv_runs_one_active_account_index)
    |> unique_constraint([:driver_account_id, :generation])
    |> check_constraint(:driver, name: :scv_runs_driver_check)
    |> check_constraint(:permission_profile, name: :scv_runs_permission_profile_check)
    |> check_constraint(:reasoning_effort, name: :scv_runs_reasoning_effort_check)
    |> check_constraint(:status, name: :scv_runs_status_check)
    |> check_constraint(:repository_revision, name: :scv_runs_repository_revision_check)
    |> check_constraint(:objective, name: :scv_runs_objective_bound_check)
  end

  @doc false
  def session_changeset(execution, attributes) do
    execution
    |> cast(attributes, [:driver_thread_id, :driver_turn_id])
    |> validate_length(:driver_thread_id, max: 256)
    |> validate_length(:driver_turn_id, max: 256)
  end

  @doc false
  def terminal_changeset(execution, attributes) do
    execution
    |> cast(attributes, [
      :status,
      :report,
      :report_digest,
      :usage,
      :resources,
      :error_code,
      :completed_at
    ])
    |> validate_required([:status, :report, :report_digest, :completed_at])
    |> validate_inclusion(:status, @terminal_statuses)
    |> validate_length(:report, min: 1, max: 32_768, count: :bytes)
    |> validate_format(:report_digest, ~r/\Asha256:[0-9a-f]{64}\z/)
    |> validate_length(:error_code, max: 80)
    |> check_constraint(:status, name: :scv_runs_status_check)
    |> check_constraint(:report, name: :scv_runs_report_bound_check)
  end
end