from __future__ import annotations import uuid from datetime import UTC, datetime from sqlalchemy import ( Boolean, DateTime, ForeignKey, Index, Integer, String, Text, TypeDecorator, UniqueConstraint, func, text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from memrelay.database import Base class UTCDateTime(TypeDecorator[datetime]): """读取时补回 UTC tzinfo 的 DateTime。 SQLite 存储 datetime 时丢弃时区信息,读回的是 naive UTC;naive 值经 isoformat() 序列化后不带偏移,Web 前端 new Date() 与 MCP 客户端会把它 误当本地时间(显示偏差 8 小时)。统一在类型层补回 UTC,序列化自动携带 +00:00,也消除 aware/naive 混合比较的隐患。 """ impl = DateTime(timezone=True) cache_ok = True def process_result_value(self, value: datetime | None, dialect: object) -> datetime | None: if value is not None and value.tzinfo is None: return value.replace(tzinfo=UTC) return value def utcnow() -> datetime: return datetime.now(UTC) def uuid_str() -> str: return str(uuid.uuid4()) class TimestampMixin: created_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow) updated_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow, onupdate=utcnow) class Administrator(TimestampMixin, Base): __tablename__ = "administrators" id: Mapped[int] = mapped_column(Integer, primary_key=True) username: Mapped[str] = mapped_column(String(100), unique=True) password_hash: Mapped[str] = mapped_column(Text) class WebSession(Base): __tablename__ = "web_sessions" id: Mapped[str] = mapped_column(String(96), primary_key=True) administrator_id: Mapped[int] = mapped_column( ForeignKey("administrators.id", ondelete="CASCADE"), index=True ) usage_profile_id: Mapped[str | None] = mapped_column( ForeignKey("usage_profiles.id", ondelete="SET NULL"), index=True ) csrf_digest: Mapped[str] = mapped_column(String(64)) expires_at: Mapped[datetime] = mapped_column(UTCDateTime(), index=True) created_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow) last_seen_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow) class McpToken(TimestampMixin, Base): __tablename__ = "mcp_tokens" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) name: Mapped[str] = mapped_column(String(100)) token_digest: Mapped[str] = mapped_column(String(64), unique=True, index=True) encrypted_token: Mapped[str] = mapped_column(Text) access_mode: Mapped[str] = mapped_column(String(16), default="read_write") all_profiles: Mapped[bool] = mapped_column(Boolean, default=False) revoked: Mapped[bool] = mapped_column(Boolean, default=False, index=True) last_used_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) usage_profile_id: Mapped[str | None] = mapped_column( ForeignKey("usage_profiles.id", ondelete="SET NULL"), index=True ) class Project(TimestampMixin, Base): __tablename__ = "projects" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) name: Mapped[str] = mapped_column(String(200)) slug: Mapped[str] = mapped_column(String(200), unique=True, index=True) git_remote: Mapped[str | None] = mapped_column(Text, unique=True) git_remote_normalized: Mapped[str | None] = mapped_column(Text, unique=True) workspace_type: Mapped[str] = mapped_column(String(32), default="general", index=True) custom_curation_template: Mapped[str | None] = mapped_column(Text) curation_enabled: Mapped[bool] = mapped_column(Boolean, default=True, index=True) source_strategy: Mapped[str] = mapped_column(String(20), default="auto") source_branch: Mapped[str | None] = mapped_column(String(200)) source_credential_item_id: Mapped[str | None] = mapped_column(String(100)) source_last_commit: Mapped[str | None] = mapped_column(String(64)) source_last_branch: Mapped[str | None] = mapped_column(String(200)) source_last_fetched_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) source_last_error: Mapped[str | None] = mapped_column(Text) source_cache_used: Mapped[bool] = mapped_column(Boolean, default=False) last_agent_sync_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) last_agent_branch: Mapped[str | None] = mapped_column(String(200)) last_agent_commit: Mapped[str | None] = mapped_column(String(64)) last_agent_dirty: Mapped[bool | None] = mapped_column(Boolean) archived: Mapped[bool] = mapped_column(Boolean, default=False, index=True) aliases: Mapped[list[ProjectAlias]] = relationship( back_populates="project", cascade="all, delete-orphan" ) class ProjectAlias(TimestampMixin, Base): __tablename__ = "project_aliases" __table_args__ = (UniqueConstraint("kind", "value", name="uq_project_alias_kind_value"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) project_id: Mapped[str] = mapped_column( ForeignKey("projects.id", ondelete="CASCADE"), index=True ) kind: Mapped[str] = mapped_column(String(32)) value: Mapped[str] = mapped_column(Text) project: Mapped[Project] = relationship(back_populates="aliases") class MemoryReference(TimestampMixin, Base): __tablename__ = "memory_references" __table_args__ = (Index("ix_memory_scope_project_type", "scope", "project_id", "memory_type"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id", ondelete="SET NULL"), index=True ) usage_profile_id: Mapped[str | None] = mapped_column( ForeignKey("usage_profiles.id", ondelete="SET NULL"), index=True ) scope: Mapped[str] = mapped_column(String(16), index=True) memory_type: Mapped[str] = mapped_column(String(32), index=True) path: Mapped[str] = mapped_column(Text, unique=True) title: Mapped[str] = mapped_column(String(300)) revision: Mapped[int] = mapped_column(Integer, default=1) status: Mapped[str] = mapped_column(String(32), default="active", index=True) tags_json: Mapped[str] = mapped_column(Text, default="[]") curation_status: Mapped[str] = mapped_column(String(32), default="pending", index=True) # Why covered_reason: distinguishes "cited by a curated document" from "reviewed by the # model with nothing to extract" so the UI can explain why a memory left the pending list. covered_reason: Mapped[str | None] = mapped_column(String(16)) curated_revision: Mapped[int | None] = mapped_column(Integer) curated_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) covered_by_json: Mapped[str] = mapped_column(Text, default="[]") superseded_by_json: Mapped[str] = mapped_column(Text, default="[]") latest_curation_job_id: Mapped[str | None] = mapped_column(String(36)) # Reviewed marks are durable job outcomes, not derived state: a model may declare a source # redundant / no_action without citing it in any document, so no document metadata can carry # the mark. Rebuild treats a matching reviewed_revision as covered with reason "reviewed". reviewed_revision: Mapped[int | None] = mapped_column(Integer) reviewed_job_id: Mapped[str | None] = mapped_column(String(36)) class IdempotencyRecord(Base): __tablename__ = "idempotency_records" __table_args__ = (UniqueConstraint("operation", "request_id", name="uq_idempotency_request"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) operation: Mapped[str] = mapped_column(String(100)) request_id: Mapped[str] = mapped_column(String(100)) response_json: Mapped[str] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow) class VaultConnection(TimestampMixin, Base): __tablename__ = "vault_connections" id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) encrypted_server_url: Mapped[str] = mapped_column(Text) encrypted_account: Mapped[str] = mapped_column(Text) encrypted_password: Mapped[str] = mapped_column(Text) login_method: Mapped[str] = mapped_column(String(16), default="password") encrypted_client_secret: Mapped[str | None] = mapped_column(Text) enabled: Mapped[bool] = mapped_column(Boolean, default=True) status: Mapped[str] = mapped_column(String(32), default="pending") last_sync_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) last_error: Mapped[str | None] = mapped_column(Text) class CredentialMapping(TimestampMixin, Base): __tablename__ = "credential_mappings" __table_args__ = (UniqueConstraint("lookup_key", name="uq_credential_mapping_lookup"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) lookup_key: Mapped[str] = mapped_column(String(300), index=True) vault_item_id: Mapped[str] = mapped_column(String(100)) class FileRecord(TimestampMixin, Base): __tablename__ = "file_records" __table_args__ = (Index("ix_file_project_status", "project_id", "status"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id", ondelete="SET NULL"), index=True ) storage_path: Mapped[str] = mapped_column(Text, unique=True) name: Mapped[str] = mapped_column(String(300), index=True) is_directory: Mapped[bool] = mapped_column(Boolean, default=False, index=True) description: Mapped[str | None] = mapped_column(Text) version: Mapped[str | None] = mapped_column(String(100), index=True) purpose: Mapped[str | None] = mapped_column(String(200), index=True) tags_json: Mapped[str] = mapped_column(Text, default="[]") mime_type: Mapped[str] = mapped_column(String(200), default="application/octet-stream") size: Mapped[int] = mapped_column(Integer) checksum_sha256: Mapped[str] = mapped_column(String(64), index=True) status: Mapped[str] = mapped_column(String(32), default="available", index=True) modified_at: Mapped[datetime] = mapped_column(UTCDateTime()) class UploadTask(TimestampMixin, Base): __tablename__ = "upload_tasks" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) target_path: Mapped[str] = mapped_column(Text) project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id", ondelete="SET NULL"), index=True ) usage_profile_id: Mapped[str | None] = mapped_column( ForeignKey("usage_profiles.id", ondelete="SET NULL"), index=True ) origin: Mapped[str] = mapped_column(String(20), default="user") expected_size: Mapped[int] = mapped_column(Integer) expected_sha256: Mapped[str | None] = mapped_column(String(64)) description: Mapped[str | None] = mapped_column(Text) version: Mapped[str | None] = mapped_column(String(100)) purpose: Mapped[str | None] = mapped_column(String(200)) tags_json: Mapped[str] = mapped_column(Text, default="[]") overwrite_allowed: Mapped[bool] = mapped_column(Boolean, default=False) token_digest: Mapped[str] = mapped_column(String(64), unique=True, index=True) expires_at: Mapped[datetime] = mapped_column(UTCDateTime(), index=True) status: Mapped[str] = mapped_column(String(32), default="pending", index=True) error_message: Mapped[str | None] = mapped_column(Text) file_id: Mapped[str | None] = mapped_column(ForeignKey("file_records.id", ondelete="SET NULL")) class SystemSetting(TimestampMixin, Base): __tablename__ = "system_settings" key: Mapped[str] = mapped_column(String(100), primary_key=True) value_json: Mapped[str] = mapped_column(Text) class UsageProfile(TimestampMixin, Base): __tablename__ = "usage_profiles" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) name: Mapped[str] = mapped_column(String(100), unique=True) description: Mapped[str | None] = mapped_column(Text) enabled: Mapped[bool] = mapped_column(Boolean, default=True, index=True) is_shared: Mapped[bool] = mapped_column(Boolean, default=False) class CurationSettings(TimestampMixin, Base): __tablename__ = "curation_settings" id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) enabled: Mapped[bool] = mapped_column(Boolean, default=False, index=True) timezone: Mapped[str] = mapped_column(String(100), default="Asia/Shanghai") output_language: Mapped[str] = mapped_column(String(16), default="auto") context_input_tokens: Mapped[int] = mapped_column(Integer, default=32000) context_output_tokens: Mapped[int] = mapped_column(Integer, default=32000) task_max_calls: Mapped[int] = mapped_column(Integer, default=20) task_max_tokens: Mapped[int] = mapped_column(Integer, default=200000) batch_chars: Mapped[int] = mapped_column(Integer, default=80000) max_merge_levels: Mapped[int] = mapped_column(Integer, default=4) max_concurrency: Mapped[int] = mapped_column(Integer, default=1) source_freshness_hours: Mapped[int] = mapped_column(Integer, default=24) retry_initial_seconds: Mapped[int] = mapped_column(Integer, default=300) retry_max_seconds: Mapped[int] = mapped_column(Integer, default=3600) max_source_files: Mapped[int] = mapped_column(Integer, default=1000) max_source_chars: Mapped[int] = mapped_column(Integer, default=20_000_000) text_file_max_bytes: Mapped[int] = mapped_column(Integer, default=2 * 1024 * 1024) rich_file_max_bytes: Mapped[int] = mapped_column(Integer, default=50 * 1024 * 1024) class CurationPolicyOverride(TimestampMixin, Base): __tablename__ = "curation_policy_overrides" key: Mapped[str] = mapped_column(String(120), primary_key=True) scope: Mapped[str] = mapped_column(String(20), index=True) workspace_type: Mapped[str | None] = mapped_column(String(32), index=True) project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id", ondelete="CASCADE"), index=True ) values_json: Mapped[str] = mapped_column(Text, default="{}") class CurationModelConfig(TimestampMixin, Base): __tablename__ = "curation_model_configs" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) revision: Mapped[int] = mapped_column(Integer, unique=True, index=True) name: Mapped[str] = mapped_column(String(100)) base_url: Mapped[str] = mapped_column(Text) encrypted_token: Mapped[str | None] = mapped_column(Text) text_model: Mapped[str] = mapped_column(String(300)) vision_model: Mapped[str | None] = mapped_column(String(300)) protocol: Mapped[str] = mapped_column(String(32), default="auto") effective_protocol: Mapped[str | None] = mapped_column(String(32)) stream: Mapped[bool] = mapped_column(Boolean, default=False) timeout_seconds: Mapped[int] = mapped_column(Integer, default=120) probe_interval_seconds: Mapped[int] = mapped_column(Integer, default=300) management_url: Mapped[str | None] = mapped_column(Text) capability_json: Mapped[str] = mapped_column(Text, default="{}") # Ordered fallback candidates for the single connection. Each item is # {"model": str, "task_classes": [...], "enabled": bool}; empty list means the # legacy single text_model chain. Part of the config revision snapshot. candidates_json: Mapped[str] = mapped_column(Text, default="[]") candidate_cooldown_seconds: Mapped[int] = mapped_column(Integer, default=600) active: Mapped[bool] = mapped_column(Boolean, default=True, index=True) class CurationModelCandidateState(Base): """Runtime health per candidate model. Kept outside CurationModelConfig on purpose: health is runtime state and must not be frozen into config revision snapshots or reset by a config save. """ __tablename__ = "curation_model_candidate_states" model_name: Mapped[str] = mapped_column(String(300), primary_key=True) cooling_until: Mapped[datetime | None] = mapped_column(UTCDateTime(), index=True) last_error_code: Mapped[str | None] = mapped_column(String(100)) last_error_message: Mapped[str | None] = mapped_column(Text) last_success_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) last_failure_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) updated_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow, onupdate=utcnow) class CurationDependencyState(TimestampMixin, Base): __tablename__ = "curation_dependency_state" id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) status: Mapped[str] = mapped_column(String(32), default="unconfigured", index=True) capability_json: Mapped[str] = mapped_column(Text, default="{}") last_check_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) last_success_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) last_failure_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) http_status: Mapped[int | None] = mapped_column(Integer) error_code: Mapped[str | None] = mapped_column(String(100)) error_message: Mapped[str | None] = mapped_column(Text) next_probe_at: Mapped[datetime | None] = mapped_column(UTCDateTime(), index=True) class CurationSchedule(TimestampMixin, Base): __tablename__ = "curation_schedules" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) name: Mapped[str] = mapped_column(String(200)) trigger_type: Mapped[str] = mapped_column(String(40), index=True) scope: Mapped[str] = mapped_column(String(16), default="project") project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id", ondelete="CASCADE"), index=True ) enabled: Mapped[bool] = mapped_column(Boolean, default=True, index=True) inheritance: Mapped[str] = mapped_column(String(20), default="instance") timezone: Mapped[str | None] = mapped_column(String(100)) recurrence_json: Mapped[str] = mapped_column(Text, default="{}") missed_policy: Mapped[str] = mapped_column(String(20), default="run_once") last_scheduled_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) last_triggered_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) next_run_at: Mapped[datetime | None] = mapped_column(UTCDateTime(), index=True) status: Mapped[str] = mapped_column(String(32), default="active") last_error: Mapped[str | None] = mapped_column(Text) class CurationChange(Base): __tablename__ = "curation_change_log" sequence: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) scope: Mapped[str] = mapped_column(String(16), index=True) project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id", ondelete="SET NULL"), index=True ) source_type: Mapped[str] = mapped_column(String(40), index=True) source_id: Mapped[str] = mapped_column(String(100), index=True) change_type: Mapped[str] = mapped_column(String(20)) revision: Mapped[str | None] = mapped_column(String(100)) content_hash: Mapped[str | None] = mapped_column(String(64)) target_hint: Mapped[str | None] = mapped_column(String(200)) summary: Mapped[str | None] = mapped_column(Text) usage_profile_id: Mapped[str | None] = mapped_column( ForeignKey("usage_profiles.id", ondelete="SET NULL"), index=True ) origin: Mapped[str] = mapped_column(String(20), index=True) observed_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow, index=True) class CurationJob(TimestampMixin, Base): __tablename__ = "curation_jobs" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) scope: Mapped[str] = mapped_column(String(16), index=True) project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id", ondelete="SET NULL"), index=True ) usage_profile_id: Mapped[str | None] = mapped_column( ForeignKey("usage_profiles.id", ondelete="SET NULL"), index=True ) mode: Mapped[str] = mapped_column(String(20), default="incremental") trigger: Mapped[str] = mapped_column(String(40), index=True) source_strategy: Mapped[str] = mapped_column(String(20), default="auto") target_documents_json: Mapped[str] = mapped_column(Text, default="[]") coalesce_key: Mapped[str | None] = mapped_column(String(200), index=True) slot: Mapped[str | None] = mapped_column(String(240), unique=True) status: Mapped[str] = mapped_column(String(32), default="queued", index=True) last_success_cursor: Mapped[int] = mapped_column(Integer, default=0) latest_observed_cursor: Mapped[int] = mapped_column(Integer, default=0) merged_trigger_count: Mapped[int] = mapped_column(Integer, default=1) trigger_summary_json: Mapped[str] = mapped_column(Text, default="{}") model_config_id: Mapped[str | None] = mapped_column( ForeignKey("curation_model_configs.id", ondelete="SET NULL") ) prompt_version: Mapped[str | None] = mapped_column(String(100)) schema_version: Mapped[str | None] = mapped_column(String(100)) lease_owner: Mapped[str | None] = mapped_column(String(100), index=True) lease_expires_at: Mapped[datetime | None] = mapped_column(UTCDateTime(), index=True) retries: Mapped[int] = mapped_column(Integer, default=0) next_retry_at: Mapped[datetime | None] = mapped_column(UTCDateTime(), index=True) error_code: Mapped[str | None] = mapped_column(String(100)) error_message: Mapped[str | None] = mapped_column(Text) stats_json: Mapped[str] = mapped_column(Text, default="{}") started_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) class CurationAttempt(Base): __tablename__ = "curation_attempts" __table_args__ = (UniqueConstraint("job_id", "attempt_number", name="uq_curation_attempt"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) attempt_key: Mapped[str] = mapped_column(String(64), unique=True, index=True) job_id: Mapped[str] = mapped_column( ForeignKey("curation_jobs.id", ondelete="CASCADE"), index=True ) attempt_number: Mapped[int] = mapped_column(Integer) model_config_id: Mapped[str | None] = mapped_column( ForeignKey("curation_model_configs.id", ondelete="SET NULL") ) effective_protocol: Mapped[str | None] = mapped_column(String(32)) model: Mapped[str | None] = mapped_column(String(300)) prompt_version: Mapped[str] = mapped_column(String(100), default="1") schema_version: Mapped[str] = mapped_column(String(100), default="1") budget_json: Mapped[str] = mapped_column(Text, default="{}") status: Mapped[str] = mapped_column(String(32), default="running") error_code: Mapped[str | None] = mapped_column(String(100)) error_message: Mapped[str | None] = mapped_column(Text) started_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow) finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) class CurationExecutionEvent(Base): __tablename__ = "curation_execution_events" __table_args__ = (Index("ix_curation_execution_events_job_created", "job_id", "created_at"),) sequence: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) job_id: Mapped[str] = mapped_column( ForeignKey("curation_jobs.id", ondelete="CASCADE"), index=True ) attempt_id: Mapped[str | None] = mapped_column( ForeignKey("curation_attempts.id", ondelete="SET NULL"), index=True ) phase: Mapped[str] = mapped_column(String(32), index=True) level: Mapped[str] = mapped_column(String(16), default="info") message_code: Mapped[str] = mapped_column(String(80)) details_json: Mapped[str] = mapped_column(Text, default="{}") created_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow) class CuratedDocument(TimestampMixin, Base): __tablename__ = "curated_documents" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) scope: Mapped[str] = mapped_column(String(16), index=True) project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id", ondelete="CASCADE"), index=True ) usage_profile_id: Mapped[str | None] = mapped_column( ForeignKey("usage_profiles.id", ondelete="SET NULL"), index=True ) document_type: Mapped[str] = mapped_column(String(100), index=True) title: Mapped[str] = mapped_column(String(300)) path: Mapped[str] = mapped_column(Text, unique=True) revision: Mapped[int] = mapped_column(Integer, default=1) latest_job_id: Mapped[str | None] = mapped_column( ForeignKey("curation_jobs.id", ondelete="SET NULL") ) source_hash: Mapped[str] = mapped_column(String(64)) source_cursor: Mapped[int] = mapped_column(Integer, default=0) metadata_json: Mapped[str] = mapped_column(Text, default="{}") model: Mapped[str | None] = mapped_column(String(300)) prompt_version: Mapped[str] = mapped_column(String(100), default="1") status: Mapped[str] = mapped_column(String(32), default="active", index=True) Index( "uq_active_curated_document", CuratedDocument.scope, func.coalesce(CuratedDocument.project_id, ""), func.coalesce(CuratedDocument.usage_profile_id, ""), CuratedDocument.document_type, unique=True, sqlite_where=text("status = 'active'"), ) class CuratedDocumentRevision(Base): __tablename__ = "curated_document_revisions" __table_args__ = ( UniqueConstraint("document_id", "revision", name="uq_curated_document_revision"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) document_id: Mapped[str] = mapped_column( ForeignKey("curated_documents.id", ondelete="CASCADE"), index=True ) revision: Mapped[int] = mapped_column(Integer) storage_path: Mapped[str] = mapped_column(Text, unique=True) job_id: Mapped[str | None] = mapped_column( ForeignKey("curation_jobs.id", ondelete="SET NULL"), index=True ) model: Mapped[str | None] = mapped_column(String(300)) source_hash: Mapped[str] = mapped_column(String(64)) metadata_json: Mapped[str] = mapped_column(Text, default="{}") change_summary: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow) class CurationSource(Base): __tablename__ = "curation_sources" __table_args__ = ( UniqueConstraint("job_id", "source_type", "source_id", name="uq_curation_source"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) job_id: Mapped[str] = mapped_column( ForeignKey("curation_jobs.id", ondelete="CASCADE"), index=True ) source_type: Mapped[str] = mapped_column(String(40)) source_id: Mapped[str] = mapped_column(String(100)) source_revision: Mapped[str | None] = mapped_column(String(100)) content_hash: Mapped[str | None] = mapped_column(String(64)) origin: Mapped[str] = mapped_column(String(20)) sent_to_model: Mapped[bool] = mapped_column(Boolean, default=False) disposition: Mapped[str] = mapped_column(String(20), default="processed") reason: Mapped[str | None] = mapped_column(Text) class ModelCall(Base): __tablename__ = "curation_model_calls" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) job_id: Mapped[str] = mapped_column( ForeignKey("curation_jobs.id", ondelete="CASCADE"), index=True ) attempt_id: Mapped[str | None] = mapped_column( ForeignKey("curation_attempts.id", ondelete="SET NULL"), index=True ) purpose: Mapped[str] = mapped_column(String(40)) protocol: Mapped[str] = mapped_column(String(32)) stream: Mapped[bool] = mapped_column(Boolean, default=False) requested_model: Mapped[str] = mapped_column(String(300)) response_model: Mapped[str | None] = mapped_column(String(300)) request_id: Mapped[str | None] = mapped_column(String(300)) prompt_version: Mapped[str] = mapped_column(String(100)) input_tokens: Mapped[int | None] = mapped_column(Integer) output_tokens: Mapped[int | None] = mapped_column(Integer) latency_ms: Mapped[int | None] = mapped_column(Integer) status: Mapped[str] = mapped_column(String(32)) error_code: Mapped[str | None] = mapped_column(String(100)) started_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow) finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) class GitConnection(TimestampMixin, Base): __tablename__ = "git_connections" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) enabled: Mapped[bool] = mapped_column(Boolean, default=False, index=True) name: Mapped[str] = mapped_column(String(100)) mode: Mapped[str] = mapped_column(String(20), default="single") remote_url: Mapped[str | None] = mapped_column(Text) username: Mapped[str | None] = mapped_column(String(500)) transport: Mapped[str] = mapped_column(String(20), default="https") credential_storage: Mapped[str] = mapped_column(String(20), default="local") encrypted_credential: Mapped[str | None] = mapped_column(Text) vault_item_id: Mapped[str | None] = mapped_column(String(100)) default_branch: Mapped[str] = mapped_column(String(200), default="main") author_name: Mapped[str] = mapped_column(String(200), default="MemRelay") author_email: Mapped[str] = mapped_column(String(320), default="memrelay@localhost") allow_write_test: Mapped[bool] = mapped_column(Boolean, default=False) allow_push_to_create: Mapped[bool] = mapped_column(Boolean, default=True) capability_json: Mapped[str] = mapped_column(Text, default="{}") last_error: Mapped[str | None] = mapped_column(Text) class MemoryRepository(TimestampMixin, Base): __tablename__ = "memory_repositories" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) connection_id: Mapped[str | None] = mapped_column( ForeignKey("git_connections.id", ondelete="SET NULL"), index=True ) mode: Mapped[str] = mapped_column(String(20)) scope: Mapped[str] = mapped_column(String(16), index=True) project_id: Mapped[str | None] = mapped_column( ForeignKey("projects.id", ondelete="SET NULL"), index=True ) local_path: Mapped[str] = mapped_column(Text, unique=True) remote_url: Mapped[str | None] = mapped_column(Text) branch: Mapped[str] = mapped_column(String(200), default="main") status: Mapped[str] = mapped_column(String(32), default="local", index=True) current_commit: Mapped[str | None] = mapped_column(String(64)) last_pushed_commit: Mapped[str | None] = mapped_column(String(64)) pending_commits: Mapped[int] = mapped_column(Integer, default=0) archived_at: Mapped[datetime | None] = mapped_column(UTCDateTime()) last_error: Mapped[str | None] = mapped_column(Text) class GitSyncJob(TimestampMixin, Base): __tablename__ = "git_sync_jobs" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) operation_id: Mapped[str] = mapped_column(String(100), unique=True, index=True) repository_id: Mapped[str] = mapped_column( ForeignKey("memory_repositories.id", ondelete="CASCADE"), index=True ) resource_id: Mapped[str | None] = mapped_column(String(100), index=True) action: Mapped[str] = mapped_column(String(40)) summary: Mapped[str] = mapped_column(String(500)) status: Mapped[str] = mapped_column(String(32), default="pending", index=True) target_commit: Mapped[str | None] = mapped_column(String(64)) attempts: Mapped[int] = mapped_column(Integer, default=0) next_retry_at: Mapped[datetime | None] = mapped_column(UTCDateTime(), index=True) lease_owner: Mapped[str | None] = mapped_column(String(140), index=True) lease_expires_at: Mapped[datetime | None] = mapped_column(UTCDateTime(), index=True) error_message: Mapped[str | None] = mapped_column(Text) class CurationScheduleTrigger(Base): __tablename__ = "curation_schedule_triggers" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid_str) schedule_id: Mapped[str] = mapped_column( ForeignKey("curation_schedules.id", ondelete="CASCADE"), index=True ) trigger_key: Mapped[str] = mapped_column(String(180), unique=True, index=True) scheduled_for: Mapped[datetime] = mapped_column(UTCDateTime(), index=True) job_ids_json: Mapped[str] = mapped_column(Text, default="[]") status: Mapped[str] = mapped_column(String(32), default="created", index=True) created_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow) class RuntimeLease(Base): __tablename__ = "runtime_leases" name: Mapped[str] = mapped_column(String(100), primary_key=True) owner: Mapped[str] = mapped_column(String(100), index=True) lease_expires_at: Mapped[datetime] = mapped_column(UTCDateTime(), index=True) updated_at: Mapped[datetime] = mapped_column(UTCDateTime(), default=utcnow)