"""SQLAlchemy table definitions for the omnigent database.""" from __future__ import annotations import contextlib import hashlib from collections.abc import Iterator from contextvars import ContextVar from typing import Any from sqlalchemy import ( BigInteger, Boolean, CheckConstraint, Float, Index, Integer, LargeBinary, SmallInteger, String, Text, UniqueConstraint, false, text, true, ) from sqlalchemy.dialects.mysql import BINARY as MySQLBinary from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from omnigent.db.compression import CompressedText # 32-byte sha256 digest column. LargeBinary → BYTEA (Postgres) / BLOB (SQLite), # but MySQL cannot index a BLOB without a key-prefix length, so use fixed-length # BINARY(32) there — an exact fit for the digest and fully indexable. _CKSUM32 = LargeBinary(32).with_variant(MySQLBinary(32), "mysql") class Base(DeclarativeBase): """Shared declarative base for all omnigent tables.""" # Default workspace id stamped on every row and used as the leading # member of every composite primary key. 0 is the single-workspace / # unassigned sentinel: with no workspace bound to the request, all rows # live in workspace 0. DEFAULT_WORKSPACE_ID = 0 # Ambient per-request workspace id. Stores are process-wide singletons, so # the active workspace can't ride on the store instance — it lives here. # OSS leaves this at the default (single-workspace 0); a multi-tenant # deployment (e.g. universe) sets it per request from the authenticated # context (via ``workspace_scope`` in middleware). Reads and inserts # resolve it through ``current_workspace_id()`` so the same store code # scopes to the caller's workspace without threading the id through every # signature — keeping this file byte-identical across deployments. _current_workspace_id: ContextVar[int] = ContextVar( "omnigent_workspace_id", default=DEFAULT_WORKSPACE_ID ) def current_workspace_id() -> int: """Return the workspace id bound to the active request/context. Defaults to :data:`DEFAULT_WORKSPACE_ID` (0) — the single-workspace OSS deployment. Multi-tenant deployments set it per request so every primary-key lookup, filter, and insert scopes to that workspace. """ return _current_workspace_id.get() @contextlib.contextmanager def workspace_scope(workspace_id: int) -> Iterator[None]: """Bind *workspace_id* for the duration of the ``with`` block. Used by multi-tenant request middleware (and tests) to scope all store access to one workspace; resets to the prior value on exit so nested / concurrent contexts don't leak. """ token = _current_workspace_id.set(workspace_id) try: yield finally: _current_workspace_id.reset(token) AGENT_KIND_TEMPLATE = "template" AGENT_KIND_SESSION = "session" POLICY_SCOPE_DEFAULT = "default" POLICY_SCOPE_SESSION = "session" class SqlAgent(Base): """ SQLAlchemy model for the ``agents`` table. Each row represents a registered agent in the system. :param id: Unique agent identifier, e.g. ``"ag_0f1a2b3c..."``. :param created_at: Unix epoch seconds when the agent was created. :param name: Human-readable agent name. Registered template agents require unique names; session-scoped copies may reuse the same name across different sessions. :param bundle_location: Artifact store key for the current bundle. Content-addressed (SHA-256 hex), e.g. ``"ag_abc123/a1b2c3d4e5f6..."``. :param version: Monotonic version counter. Starts at 1, incremented on each update via ``PUT /api/agents/{id}``. :param kind: ``"template"`` for server-wide registered agents; ``"session"`` for per-conversation copies. :param description: Optional free-text description of the agent's purpose. ``None`` when not provided. :param updated_at: Unix epoch seconds of the last update, or ``None`` if the agent has never been updated. """ __tablename__ = "agents" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) id: Mapped[str] = mapped_column(String(64), primary_key=True) created_at: Mapped[int] = mapped_column(Integer) name: Mapped[str] = mapped_column(String(256)) bundle_location: Mapped[str] = mapped_column(String(512)) version: Mapped[int] = mapped_column(Integer, default=1) # Enum stored as a stable int code (see omnigent.db.enum_codecs # AGENT_KIND: template=1, session=2). The store converts to/from the # string name at the row↔entity boundary. kind: Mapped[int] = mapped_column(SmallInteger) description: Mapped[str | None] = mapped_column(CompressedText, nullable=True) updated_at: Mapped[int | None] = mapped_column(Integer, nullable=True) __table_args__ = ( CheckConstraint("kind IN (1, 2)", name="ck_agents_kind"), Index("ix_agents_created_at", "workspace_id", "created_at", "id"), # Template agents have unique names; session-scoped agents (kind=2) # may reuse the same name. That "unique only within the template set" # rule can't be a partial unique index (MySQL has none), so it is # enforced in the store (SqlAlchemyAgentStore.create). This plain index # backs the (workspace_id, name, kind) lookup that check and get_by_name # do — kind is included so the seek skips same-named session copies # straight to the template row. Index("ix_agents_name", "workspace_id", "name", "kind", "id"), ) class SqlFile(Base): """ SQLAlchemy model for the ``files`` table. Each row represents an uploaded file tracked by the system. :param id: Unique file identifier, e.g. ``"file_a1b2c3d4..."``. :param created_at: Unix epoch seconds when the file record was created. :param filename: Original filename as provided by the uploader, max 512 characters. e.g. ``"report.pdf"``. :param bytes: Size of the file in bytes. :param content_type: MIME type of the file, e.g. ``"application/pdf"``. ``None`` when not provided. """ __tablename__ = "files" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) id: Mapped[str] = mapped_column(String(64), primary_key=True) created_at: Mapped[int] = mapped_column(Integer) filename: Mapped[str] = mapped_column(String(512)) bytes: Mapped[int] = mapped_column(Integer) content_type: Mapped[str | None] = mapped_column(String(256), nullable=True) session_id: Mapped[str | None] = mapped_column(String(64), nullable=True) __table_args__ = ( Index("ix_files_created_at", "workspace_id", "created_at", "id"), Index( "ix_files_session_id_created_at", "workspace_id", "session_id", "created_at", "id", ), ) class SqlUser(Base): """ SQLAlchemy model for the ``users`` table. Each row represents a user. In header / OIDC modes, ``id`` is the upstream identity (email or ``"local"``); the row is upserted on first sight and ``password_hash`` stays ``NULL``. In ``accounts`` mode, rows are created explicitly by the admin or via invite redemption with a populated ``password_hash``. :param id: User identifier — email in header/OIDC modes, chosen username in accounts mode, ``"local"`` in single-user. :param is_admin: When ``True``, the user bypasses all permission checks. ``False`` by default. :param password_hash: argon2id hash of the user's password. ``NULL`` for users created via header/OIDC modes (their password is the upstream IdP's). :param created_at: Unix epoch seconds when the row was inserted. Populated for accounts-mode users; ``NULL`` for legacy rows backfilled by the original permissions migration. :param last_login_at: Unix epoch seconds of the most recent successful ``/auth/login`` (accounts mode). ``NULL`` until the first login. """ __tablename__ = "users" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) id: Mapped[str] = mapped_column(String(128), primary_key=True) is_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=false()) password_hash: Mapped[str | None] = mapped_column(String(256), nullable=True) created_at: Mapped[int | None] = mapped_column(Integer, nullable=True) last_login_at: Mapped[int | None] = mapped_column(Integer, nullable=True) class SqlAccountToken(Base): """ SQLAlchemy model for the ``account_tokens`` table. Backs both invite tokens (admin-issued, allow self-serve registration) and magic-login tokens (CLI-minted, hand off a signed-in session into the web UI). Both have the same short-TTL single-use lifecycle, so they share one table. :param id: Opaque random token string (43+ URL-safe base64 chars). This is the secret — the user presents it as a query param. Stored verbatim because we need constant-time lookup; rotation = delete + recreate. :param kind: ``"invite"`` (anyone can redeem; creates a new user) or ``"magic"`` (the bound ``user_id`` is signed in). :param user_id: For ``magic``, the user the token signs in as. For ``invite``, ``NULL`` (the username is chosen at redemption time). :param created_by: User id of the admin who issued an invite (``NULL`` for magic tokens, which are self-issued). :param created_at: Unix epoch seconds when the token was minted. ``expires_at = created_at + ttl_seconds``. :param expires_at: Unix epoch seconds when the token stops being redeemable. Single-use enforcement is via ``redeemed_at``, this just bounds the window. :param redeemed_at: Unix epoch seconds when the token was consumed. ``NULL`` until then. After being set, the token is dead — redeem checks this column atomically. :param invited_is_admin: For invite tokens, whether the resulting user should be created with admin rights. False for magic tokens. """ __tablename__ = "account_tokens" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) id: Mapped[str] = mapped_column(String(128), primary_key=True) # Enum stored as a stable int code (see omnigent.db.enum_codecs # ACCOUNT_TOKEN_KIND: invite=1, magic=2). The store converts to/from # the string name at the row↔entity boundary. kind: Mapped[int] = mapped_column(SmallInteger, nullable=False) user_id: Mapped[str | None] = mapped_column(String(128), nullable=True) created_by: Mapped[str | None] = mapped_column(String(128), nullable=True) created_at: Mapped[int] = mapped_column(Integer, nullable=False) expires_at: Mapped[int] = mapped_column(Integer, nullable=False) redeemed_at: Mapped[int | None] = mapped_column(Integer, nullable=True) invited_is_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=false()) __table_args__ = ( CheckConstraint("kind IN (1, 2)", name="ck_account_tokens_kind"), Index("ix_account_tokens_expires_at", "workspace_id", "expires_at", "id"), ) class SqlSessionPermission(Base): """ SQLAlchemy model for the ``session_permissions`` table. Junction table mapping ``(user_id, conversation_id)`` to a numeric permission level. PK is ``(user_id, conversation_id)`` — optimized for the hot path ("list sessions I can access" = prefix scan on ``user_id``). The ``"__public__"`` sentinel ``user_id`` represents public read access to a session. :param user_id: The grantee, e.g. ``"alice@example.com"`` or ``"__public__"`` for public access. :param conversation_id: The session being shared, e.g. ``"conv_e4f5a6b7..."``. :param level: Numeric permission level: ``1`` = read, ``2`` = edit, ``3`` = manage. Each level subsumes the ones below it (comparison is ``>=``). """ __tablename__ = "session_permissions" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) user_id: Mapped[str] = mapped_column( String(128), primary_key=True, ) conversation_id: Mapped[str] = mapped_column( String(64), primary_key=True, ) level: Mapped[int] = mapped_column(Integer, nullable=False) __table_args__ = ( CheckConstraint("level IN (1, 2, 3, 4)", name="ck_session_permissions_level"), # Lookups by conversation (get_session_owner) filter workspace_id + # conversation_id; user_id trails to complete the PK. Index( "ix_session_permissions_conversation_id", "workspace_id", "conversation_id", "user_id", ), ) class SqlConversation(Base): """ SQLAlchemy model for the ``conversations`` table. Each row represents a conversation thread that contains one or more conversation items. :param id: Unique conversation identifier, e.g. ``"conv_e4f5a6b7..."``. :param created_at: Unix epoch seconds when the conversation was created. :param updated_at: Unix epoch seconds when the conversation was last updated (item append, title change, etc.). :param title: Human-readable title; empty string when untitled. :param kind: Conversation type. ``"default"`` for user-initiated, ``"sub_agent"`` for sub-agent execution conversations. :param parent_conversation_id: For Phase 4 named sub-agents, points at the parent conversation. ``None`` for top-level conversations. ``ON DELETE CASCADE`` so removing a parent cleans up the entire sub-tree. :param root_conversation_id: Id of the root (top-level) conversation in the spawn tree. Equal to ``id`` for top-level conversations. Indexed so ``sys_session_get_history`` / ``sys_session_close`` can verify that a target ``conversation_id`` lives in the caller's tree in O(1) — any agent in the tree can address any other by ``conversation_id``. ``ON DELETE CASCADE`` to keep it consistent with ``parent_conversation_id`` when a root is deleted. :param agent_id: Foreign key to the agent bound to this conversation at creation time. ``None`` for legacy conversations created without an agent binding (these are excluded from ``GET /v1/sessions`` results). :param runner_id: Runner the conversation is pinned to (hard affinity per ``designs/RUNNER.md`` §5). ``None`` until the first dispatch claims a runner; thereafter every subsequent dispatch routes to this runner while it is online (or fails with ``runner_unavailable`` if it isn't). No FK because runner records are not persisted in v1 — the registry is purely in-memory. :param external_session_id: Runtime-native session id this conversation wraps, e.g. Claude Code's session uuid for ``omnigent claude`` sessions. ``None`` for regular AP-only conversations. Populated by the wrapper bridge from the underlying runtime and used by ``--resume`` to recover the external session's prior transcript. Generic across runtimes — at most one external session per conversation. No FK because the id is generated externally (by Claude Code, Codex, Pi, etc.) and is not tracked in any AP-side table. :param workspace: Absolute path on disk where the runner should start, e.g. ``"/Users/corey/universe/src/foo"``. Required when ``host_id`` is set (enforced by check constraint ``ck_conversations_workspace_required_for_host``); optional for CLI-launched sessions that record their starting cwd for display. Stored as the canonicalized realpath returned by ``host.stat`` at session-create time; runtime symlinks are pre-resolved so the boundary check on the agent's ``os_env.cwd`` cannot be smuggled past via a symlink. Immutable after creation — designs/SESSION_WORKSPACE_SELECTION.md. When a git worktree was created for the session, this is the worktree directory path rather than the picked source repo. :param git_branch: Git branch checked out in the session's worktree, e.g. ``"feature/login"``. Set only when the session was created with a server-created git worktree; ``None`` otherwise. ``git_branch IS NOT NULL`` gates worktree cleanup on session delete. See designs/SESSION_GIT_WORKTREE.md. :param archived: Whether the session is archived. Archived sessions are hidden from the default ``GET /v1/sessions`` listing (and the sidebar); the listing returns them only when ``include_archived=True``. ``False`` for normal sessions. Reversible via ``PATCH /v1/sessions/{id}``. """ __tablename__ = "conversations" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) id: Mapped[str] = mapped_column(String(64), primary_key=True) created_at: Mapped[int] = mapped_column(Integer) updated_at: Mapped[int] = mapped_column(Integer) title: Mapped[str] = mapped_column(String(768), nullable=False, server_default="") # Enum stored as a stable int code (see omnigent.db.enum_codecs # CONVERSATION_KIND: default=1, sub_agent=2). The store converts to/from # the string name at the row↔entity boundary. kind: Mapped[int] = mapped_column(SmallInteger, default=1) parent_conversation_id: Mapped[str | None] = mapped_column( String(64), nullable=True, ) root_conversation_id: Mapped[str] = mapped_column( String(64), nullable=False, ) agent_id: Mapped[str | None] = mapped_column( String(64), nullable=True, ) runner_id: Mapped[str | None] = mapped_column(String(64), nullable=True) # Host that launched (or should launch) the runner for this # session. Set when a session is created via the Web UI on a # specific host. No FK: host records are managed outside this # table; deletion is handled explicitly by the application. host_id: Mapped[str | None] = mapped_column( String(64), nullable=True, ) # Per-session reasoning-effort hint, e.g. "high". Nullable; # None means use the agent default. reasoning_effort: Mapped[str | None] = mapped_column(String(32), nullable=True) # Per-session LLM model override, e.g. "claude-opus-4-7". Nullable; # None means use the agent default from the spec. model_override: Mapped[str | None] = mapped_column(String(128), nullable=True) # Per-session cost-control switch: "on" | "off". Nullable; None # means use the spec default (see entities.Conversation). cost_control_mode_override: Mapped[str | None] = mapped_column(String(8), nullable=True) # Per-session brain-harness override, e.g. "pi". Nullable; None # means use the spec's executor.config.harness (see entities.Conversation). harness_override: Mapped[str | None] = mapped_column(String(64), nullable=True) # Sub-agent type name within the parent's spec tree, e.g. # "summarizer". The runner uses this to load the sub-agent's # AgentSpec instead of the parent's. Replaces task.agent_name # from the removed task store. None for top-level sessions. sub_agent_name: Mapped[str | None] = mapped_column(String(128), nullable=True) # Monotonic allocator for the next item position in this conversation. # append() reads and advances this instead of scanning # MAX(SqlConversationItem.position) on every write, making position # assignment O(1) and collision-free under the conversation lock. New rows # start at 0 (column default); NULL marks a row created before this column # existed, which append() backfills via a one-time scan on its next write. next_position: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0) external_session_id: Mapped[str | None] = mapped_column(String(128), nullable=True) # JSON-serialized mutable per-conversation key/value store # used by policy callables to accumulate state across turns. # NULL when no policy has written state yet; empty JSON object # "{}" is equivalent. Stored as Text (not a native JSON column) # for SQLite compatibility. session_state: Mapped[str | None] = mapped_column(CompressedText, nullable=True) # JSON-serialized cumulative LLM token usage for policy # callables. Shape: {"input_tokens": N, "output_tokens": M, # "total_tokens": T, "cache_read_input_tokens": C1, # "cache_creation_input_tokens": C2, "total_cost_usd": X}. # NULL when no LLM calls have been recorded yet. session_usage: Mapped[str | None] = mapped_column(CompressedText, nullable=True) # Pass-through CLI args for a native terminal wrapper (claude / # codex), JSON-encoded list of strings, e.g. # '["--dangerously-skip-permissions"]'. NULL for non-native # sessions. The runner reconstructs the terminal launch command # from these plus the harness binary; the command itself and all # bridge / AP-URL / auth wiring are runner-owned and never stored # here. A flat list (not a dict) is deliberate: there is no key for # a user to smuggle internal wiring through. See # designs/NATIVE_RUNNER_SERVER_LAUNCH.md. terminal_launch_args: Mapped[str | None] = mapped_column(CompressedText, nullable=True) # Absolute path on the host where the runner cd's. Required # when host_id is set; CHECK constraint below. When a git worktree # was created for the session, this is the worktree directory path. workspace: Mapped[str | None] = mapped_column(String(2048), nullable=True) # Git branch checked out in the session's worktree, e.g. # "feature/login". Set only when the session was created with a # server-created git worktree; None otherwise. Gates worktree # cleanup on delete. See designs/SESSION_GIT_WORKTREE.md. git_branch: Mapped[str | None] = mapped_column(String(255), nullable=True) # Whether the session is archived (hidden from the default # /v1/sessions listing and the sidebar). False for normal # sessions; server_default false backfills existing rows on the # migration that adds this column. Low-cardinality, so no index — # the listing's accessible_by subquery is the selective filter. archived: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, server_default=false() ) __table_args__ = ( CheckConstraint("kind IN (1, 2)", name="ck_conversations_kind"), CheckConstraint( "host_id IS NULL OR workspace IS NOT NULL", name="ck_conversations_workspace_required_for_host", ), Index("ix_conversations_created_at", "workspace_id", "created_at", "id"), Index("ix_conversations_updated_at", "workspace_id", "updated_at", "id"), Index("ix_conversations_kind", "workspace_id", "kind", "id"), # Agent lookups: find the conversation(s) that own a given agent. Index("ix_conversations_agent_id", "workspace_id", "agent_id", "id"), Index( "ix_conversations_root_conversation_id", "workspace_id", "root_conversation_id", "id", ), # Reconnect/relaunch reconciliation looks up a runner's session(s) # by runner_id (list_conversations_by_runner_id) on every runner # reconnect; index it to avoid a full scan. Index("ix_conversations_runner_id", "workspace_id", "runner_id", "id"), # Unique index on (parent_conversation_id, title) prevents two # same-named children under the same parent (G36 race protection at # the DB layer). Top-level conversations (NULL parent) are exempt # automatically: NULLs are distinct in a unique index, so no WHERE # predicate is needed — keeping it a plain index MySQL can build. Index( "ix_conversations_parent_title_unique", "workspace_id", "parent_conversation_id", "title", unique=True, mysql_length={"title": 512}, ), # Composite index for child-session listing # (list_conversations(kind="sub_agent", parent_conversation_id=...)). # Non-unique, so no scoping predicate is required; it simply indexes # every parented row rather than only the sub-agent ones. Index( "idx_conversations_parent", "workspace_id", "parent_conversation_id", text("created_at DESC"), text("id DESC"), ), ) class SqlConversationItem(Base): """ SQLAlchemy model for the ``conversation_items`` table. Each row represents a single item (message, function call, function call output, or reasoning block) within a conversation. :param id: Unique item identifier with a type-based prefix, e.g. ``"msg_a1b2c3..."``, ``"fc_d4e5f6..."``. :param conversation_id: Foreign key to :class:`SqlConversation.id`. Cascades on delete. :param response_id: The task/response ID this item belongs to, e.g. ``"resp_d8e9f0a1..."``. :param created_at: Unix epoch seconds when the item was created. :param status: Item status string. Defaults to ``"completed"``. :param position: Zero-based ordering index within the conversation. Used for deterministic item ordering. :param type: Item type discriminator, one of ``"message"``, ``"function_call"``, ``"function_call_output"``, ``"reasoning"``. :param data: JSON-serialized item payload. Structure varies by ``type``. :param search_text: Plain-text extraction of ``data`` used for full-text search indexing. :param created_by: Identity of the human actor who authored the item, or ``None`` for agent/tool/system items and single-user mode. Mirrors :class:`SqlComment.created_by`. """ __tablename__ = "conversation_items" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) # conversation_id leads id in the PK so a conversation's items stay # contiguous for the per-conversation prefix scans that dominate reads. conversation_id: Mapped[str] = mapped_column( String(64), primary_key=True, ) id: Mapped[str] = mapped_column(String(64), primary_key=True) response_id: Mapped[str] = mapped_column(String(64)) created_at: Mapped[int] = mapped_column(Integer) # Enum stored as a stable int code (see omnigent.db.enum_codecs # ITEM_STATUS: completed=1). Only "completed" is written today, but the # CHECK admits the wider OpenAI-style status vocabulary reserved there. status: Mapped[int] = mapped_column(SmallInteger, default=1) position: Mapped[int] = mapped_column(Integer) # Enum stored as a stable int code (see omnigent.db.enum_codecs # ITEM_TYPE). The store converts to/from the string name at the # row↔entity boundary. type: Mapped[int] = mapped_column(SmallInteger) data: Mapped[str] = mapped_column(Text) search_text: Mapped[str] = mapped_column(Text) created_by: Mapped[str | None] = mapped_column(String(128), nullable=True) __table_args__ = ( Index( "ix_conversation_items_conversation_id_position", "workspace_id", "conversation_id", "position", unique=True, ), # Fork-truncation looks up by workspace_id + conversation_id + # response_id; id trails to complete the PK. Index( "ix_conversation_items_response_id", "workspace_id", "conversation_id", "response_id", "id", ), CheckConstraint( "type IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)", name="ck_conversation_items_type", ), CheckConstraint("status IN (1, 2, 3, 4)", name="ck_conversation_items_status"), ) # Width of the ``conversation_labels.value`` column. Exported so the store # (and the session-status error-label path) can clamp values to fit instead # of letting an over-length write raise ``DataError`` on PostgreSQL. LABEL_VALUE_MAX_LEN = 256 class SqlConversationLabel(Base): """ SQLAlchemy model for the ``conversation_labels`` table. One row per (conversation, label-key) pair. Labels live in a dedicated table rather than a JSON column on ``conversations`` so per-key UPDATEs are atomic without read-modify-write (see POLICIES.md §6). The table is keyed only by ``conversation_id`` + ``key``, so it is untouched by compaction (which rewrites ``conversation_items``) — labels set turn 3 still exist turn 20 even after the earlier turns have been folded into a summary. :param conversation_id: The conversation this label belongs to. Composite PK member. Deleted with the conversation via ``ON DELETE CASCADE``. :param key: The label key, e.g. ``"integrity"``, ``"sensitivity"``. Composite PK member. :param value: The label value as a string, e.g. ``"0"``, ``"confidential"``. All label values are string-typed regardless of what the YAML author wrote — the parser coerces scalar / list values during spec load (POLICIES.md §14). :param updated_at: Unix epoch seconds of the last write. Single timestamp for each row; on UPSERT the row's timestamp is refreshed even when the value is unchanged (matches omnigent parity and keeps debugging timelines accurate). """ __tablename__ = "conversation_labels" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) conversation_id: Mapped[str] = mapped_column( String(64), primary_key=True, ) key: Mapped[str] = mapped_column(String(128), primary_key=True) value: Mapped[str] = mapped_column(String(LABEL_VALUE_MAX_LEN)) updated_at: Mapped[int] = mapped_column(Integer) class SqlComment(Base): """SQLAlchemy model for the ``comments`` table. Stores per-review comments associated with a conversation. Each comment is anchored to a character range in the file expressed as absolute document-level offsets. Comments survive server restarts and are cleaned up when the owning conversation is deleted. :param id: UUID primary key, e.g. ``"a1b2c3d4-..."``. :param conversation_id: The conversation this comment belongs to. :param path: File path relative to the workspace root, e.g. ``"src/App.tsx"``. :param start_index: 0-based absolute character offset (inclusive) within the file where the anchor range begins. :param end_index: 0-based absolute character offset (exclusive) within the file where the anchor range ends. :param body: The comment text. :param status: One of ``"draft"``, ``"addressed"``. :param created_at: Unix epoch seconds at row creation. :param updated_at: Unix epoch **microseconds** of the last body/status mutation; set at creation for never-edited comments. Feeds the per-session comments fingerprint surfaced on ``GET /v1/sessions`` so clients can detect comment changes; microsecond precision keeps back-to-back mutations within one second distinguishable while remaining an exact integer in JavaScript. ``BigInteger`` because epoch-µs overflows a 32-bit column on PostgreSQL. :param anchor_content: Plain-text snapshot of the selected range at comment creation time. Used to re-anchor the comment (e.g. via content search) when the file is subsequently edited. ``NULL`` for legacy comments created before anchor support. :param created_by: Email of the user who created this comment, e.g. ``"alice@example.com"``. ``NULL`` for legacy comments or comments created in single-user mode. """ __tablename__ = "comments" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) id: Mapped[str] = mapped_column(String(64), primary_key=True) conversation_id: Mapped[str] = mapped_column(String(64)) path: Mapped[str] = mapped_column(String(4096)) start_index: Mapped[int] = mapped_column(Integer) end_index: Mapped[int] = mapped_column(Integer) body: Mapped[str] = mapped_column(CompressedText) # Enum stored as a stable int code (see omnigent.db.enum_codecs # COMMENT_STATUS: draft=1, addressed=2). status: Mapped[int] = mapped_column(SmallInteger) created_at: Mapped[int] = mapped_column(Integer) updated_at: Mapped[int] = mapped_column(BigInteger) anchor_content: Mapped[str | None] = mapped_column(CompressedText, nullable=True) created_by: Mapped[str | None] = mapped_column(String(128), nullable=True) __table_args__ = ( CheckConstraint("status IN (1, 2)", name="ck_comments_status"), # Serves list_for_conversation: WHERE workspace_id + conversation_id # ORDER BY created_at, id. Folds created_at in (over a bare # conversation_id index) so the sort is index-ordered; trails id to # complete the PK. Index( "ix_comments_conversation_id", "workspace_id", "conversation_id", "created_at", "id", ), ) def policy_name_cksum(name: str) -> bytes: """Return the sha256 digest of a policy name. This 32-byte digest is what the name-uniqueness indexes key on instead of the raw ``VARCHAR(256)`` name — a fixed, compact index entry. Two names collide iff their digests do, so uniqueness is preserved. """ return hashlib.sha256(name.encode("utf-8")).digest() def _default_policy_name_cksum(context: Any) -> bytes: """Column default: derive ``name_cksum`` from the bound ``name`` on INSERT. Mirrors the ``workspace_id`` default pattern so every ORM insert stamps the checksum without the caller setting it. Column defaults do not fire on UPDATE, so renames recompute it explicitly in the store. """ return policy_name_cksum(context.get_current_parameters()["name"]) class SqlPolicy(Base): """ SQLAlchemy model for the ``policies`` table. Policies are either session-scoped (``session_id`` set, FK to ``conversations.id``) or server-wide defaults (``session_id IS NULL``). Session-scoped policies are created via ``POST /v1/sessions/{session_id}/policies``. Default policies are created via ``POST /v1/policies``. :param id: Opaque PK, e.g. ``"pol_a1b2c3..."``. :param name: Human-readable name. UNIQUE per session for session policies; globally unique for default policies (``session_id IS NULL``). Uniqueness is enforced on ``name_cksum`` rather than this column. :param name_cksum: sha256 digest of ``name`` (32 bytes). The name-uniqueness indexes key on this compact digest instead of the wide ``VARCHAR(256)`` name. Stamped on INSERT by a column default; recomputed by the store on rename. :param session_id: FK to ``conversations.id``. ``None`` for server-wide default policies. ``ON DELETE CASCADE`` so removing a session cleans up its policies. :param created_at: Unix epoch seconds at row creation. :param updated_at: Unix epoch seconds of the last write, ``None`` if the row has never been updated. :param type: Handler discriminator: ``"python"``, ``"url"``. :param handler: Dotted import path (``type="python"``) or HTTPS URL (``type="url"``). :param factory_params: JSON-encoded dict of kwargs passed to the handler when it is a factory function. ``None`` when the handler is a direct callable or for ``type="url"``. :param enabled: Whether the engine consults this row. Defaults to true. :param scope: ``"default"`` for server-wide policies; ``"session"`` for session-scoped policies. Explicit discriminator so queries filter by column value instead of checking ``session_id IS NULL``. :param created_by: User ID of the admin who created this policy. ``None`` in single-user mode or for session-scoped policies. """ __tablename__ = "policies" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) id: Mapped[str] = mapped_column(String(64), primary_key=True) name: Mapped[str] = mapped_column(String(256)) # sha256(name) — the value the name-uniqueness indexes key on instead of # the wide name column. Stamped from `name` on INSERT via the column # default; the store recomputes it on rename (defaults don't fire on UPDATE). name_cksum: Mapped[bytes] = mapped_column(_CKSUM32, default=_default_policy_name_cksum) # Nullable: NULL for server-wide default policies. session_id: Mapped[str | None] = mapped_column( String(64), nullable=True, ) created_at: Mapped[int] = mapped_column(Integer) updated_at: Mapped[int | None] = mapped_column(Integer, nullable=True) # Handler discriminator stored as a stable int code (see # omnigent.db.enum_codecs POLICY_TYPE: python=1, url=2). type: Mapped[int] = mapped_column(SmallInteger) # Dotted import path (type="python") or HTTPS URL # (type="url") for the policy handler. handler: Mapped[str] = mapped_column(Text) # JSON-encoded dict of factory kwargs for type="python" when # the handler is a factory function. NULL when the handler is # a direct callable or for type="url". See the design doc's # FunctionRef.arguments pattern. factory_params: Mapped[str | None] = mapped_column(Text, nullable=True) enabled: Mapped[bool] = mapped_column(Boolean, server_default=true()) # "default" for server-wide policies; "session" for per-conversation # copies. Mirrors the agents.kind pattern so queries filter by column # value rather than session_id IS NULL. Enum stored as a stable int # code (see omnigent.db.enum_codecs POLICY_SCOPE: default=1, session=2). scope: Mapped[int] = mapped_column(SmallInteger) created_by: Mapped[str | None] = mapped_column(String(128), nullable=True) __table_args__ = ( CheckConstraint("type IN (1, 2)", name="ck_policies_type"), CheckConstraint("scope IN (1, 2)", name="ck_policies_scope"), Index("ix_policies_created_at", "workspace_id", "created_at", "id"), Index("ix_policies_session_id", "workspace_id", "session_id", "id"), # Name uniqueness keys on name_cksum (sha256 of name) rather than the # wide name column, for a compact 32-byte index entry. UniqueConstraint( "workspace_id", "session_id", "name_cksum", name="uq_policies_session_id_name_cksum", ), # Default policies must have unique names; session-scoped policies # may reuse the same name. That "unique only within the default set" # rule can't be a partial unique index (MySQL has none), so it is # enforced in the store (add_default / update_default). This plain # index just backs the name_cksum lookup those checks perform. Index("ix_policies_name_cksum", "workspace_id", "name_cksum", "id"), ) class SqlHost(Base): """ SQLAlchemy model for the ``hosts`` table. Each row represents a machine that has connected to the server via ``omnigent host``. The row is upserted on first connect and updated on subsequent reconnects (name, status, timestamps). :param host_id: Stable host identifier from the host's local ``~/.omnigent/config.yaml``, e.g. ``"host_a1b2c3d4e5f6..."``. :param name: Human-readable name from ``config.yaml``, e.g. ``"corey-laptop"``. Displayed in the Web UI host picker. Max 64 characters. :param owner: User ID from the Databricks auth Bearer token presented during the host's WebSocket handshake, e.g. ``"corey.zumar@databricks.com"``. :param status: ``"online"`` when the host has an active WebSocket connection, ``"offline"`` when disconnected. :param created_at: Unix epoch seconds when the host was first registered (first ``omnigent host``). :param updated_at: Unix epoch seconds the row was last touched — a status change (connect/disconnect) or a tunnel heartbeat. Doubles as the host's last-seen for the liveness freshness gate, so a host that crashed without a graceful disconnect ages out of the "online" set once this stops advancing. :param token_hash: Hex SHA-256 digest of the launch token that authenticates a SERVER-MANAGED sandbox host's tunnel connection (``host_type="managed"`` sessions) — never the raw token. ``NULL`` for external (user-connected) hosts. Overwritten when the sandbox is relaunched, which atomically revokes the previous generation's token. :param token_expires_at: Unix epoch seconds after which the launch token no longer authenticates. Scoped to the TOKEN, not the host — the host row is durable across sandbox generations; the expiry is set past the provider's maximum sandbox lifetime so a live sandbox can always reconnect while a token leaked from a dead one cannot. ``NULL`` for external hosts. :param sandbox_provider: Sandbox provider backing a managed host, e.g. ``"modal"``. ``NULL`` for external hosts — non-NULL is the "this host is server-managed" discriminator. :param sandbox_id: Provider-assigned id of the sandbox currently backing the host, e.g. ``"sb-a1b2c3"`` — what termination is issued against. ``NULL`` for external hosts. :param configured_harnesses: JSON-encoded per-harness readiness map reported in the host's last ``host.hello`` frame, e.g. ``'{"claude-sdk": true, "codex": false}'``. ``NULL`` when the host has never reported it (older host build) — unknown, not "nothing configured". Surfaced via ``GET /v1/hosts`` so the web agent picker can warn about unconfigured harnesses. """ __tablename__ = "hosts" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) host_id: Mapped[str] = mapped_column(String(64), primary_key=True) owner: Mapped[str] = mapped_column(String(256), nullable=False) name: Mapped[str] = mapped_column(String(64), nullable=False) # Enum stored as a stable int code (see omnigent.db.enum_codecs # HOST_STATUS: online=1, offline=2). status: Mapped[int] = mapped_column(SmallInteger) created_at: Mapped[int] = mapped_column(Integer) updated_at: Mapped[int] = mapped_column(Integer) token_hash: Mapped[str | None] = mapped_column(String(64), nullable=True) token_expires_at: Mapped[int | None] = mapped_column(Integer, nullable=True) sandbox_provider: Mapped[str | None] = mapped_column(String(32), nullable=True) sandbox_id: Mapped[str | None] = mapped_column(String(256), nullable=True) configured_harnesses: Mapped[str | None] = mapped_column(Text, nullable=True) __table_args__ = ( CheckConstraint( "status IN (1, 2)", name="ck_hosts_status", ), # (workspace_id, owner, name) was the old PK; keep it unique so the # upsert-on-connect logic (look up by owner+name to detect host_id # rotation) stays consistent. UniqueConstraint("workspace_id", "owner", "name", name="uq_hosts_workspace_owner_name"), # resolve_launch_token filters workspace_id + token_hash, so scoping # the unique to the workspace keeps that lookup index-served. UniqueConstraint("workspace_id", "token_hash", name="uq_hosts_token_hash"), ) class SqlUserDailyCost(Base): """ SQLAlchemy model for the ``user_daily_cost`` table. A running per-user, per-UTC-day rollup of LLM spend, used by cost-aware policies (e.g. the "downgrade expensive model once a user has spent >$X today" sample policy) to read a user's accumulated daily cost as a single O(1) point lookup instead of aggregating the per-session ``conversations.session_usage`` blobs on every policy evaluation. One row per ``(user_id, day_utc)``. Incremented (UPSERT ``cost_usd = cost_usd + delta``) at each turn boundary from the cost write sites — but only when the session runs under at least one policy, so the table is never touched in deployments that have no policies configured (this keeps the shared server code inert against a database that lacks this table). :param user_id: The user the cost is attributed to — the session creator (``LEVEL_OWNER`` grantee), e.g. ``"alice@example.com"``. :param day_utc: The UTC calendar day the spend occurred, as an ISO date string ``"YYYY-MM-DD"``, e.g. ``"2026-06-05"``. Bucketed by the turn's wall-clock time, so a session spanning midnight splits its cost across both days correctly. :param cost_usd: Cumulative USD spend for this user on this day. Starts at the first turn's delta and grows by each subsequent turn's delta. :param ask_approved_usd: Highest soft warning checkpoint (USD) the user has already approved continuing past for this day — read and written by the per-user daily cost-budget policy so an approved checkpoint prompts at most once per day (across all of the user's sessions), not once per session. ``0.0`` (the server default) means no checkpoint approved yet. :param updated_at: Unix epoch seconds of the last increment. """ __tablename__ = "user_daily_cost" # Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK. workspace_id: Mapped[int] = mapped_column( BigInteger, primary_key=True, nullable=False, server_default="0", default=current_workspace_id, ) user_id: Mapped[str] = mapped_column(String(128), primary_key=True) day_utc: Mapped[str] = mapped_column(String(10), primary_key=True) cost_usd: Mapped[float] = mapped_column(Float, nullable=False) ask_approved_usd: Mapped[float] = mapped_column(Float, nullable=False, server_default="0") updated_at: Mapped[int] = mapped_column(Integer)