feat: 导入 MemRelay 初始源码

This commit is contained in:
Nixevol
2026-09-23 21:40:46 +08:00
commit 0ade3bb318
167 changed files with 53511 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from memrelay import models # noqa: F401
from memrelay.database import Base
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
context.configure(
url=config.get_main_option("sqlalchemy.url"),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+25
View File
@@ -0,0 +1,25 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
@@ -0,0 +1,197 @@
"""Create the initial MemRelay schema."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260802_0001"
down_revision: str | Sequence[str] | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"administrators",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("username", sa.String(100), nullable=False, unique=True),
sa.Column("password_hash", sa.Text(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_table(
"web_sessions",
sa.Column("id", sa.String(96), primary_key=True),
sa.Column(
"administrator_id",
sa.Integer(),
sa.ForeignKey("administrators.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("csrf_digest", sa.String(64), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_web_sessions_administrator_id", "web_sessions", ["administrator_id"])
op.create_index("ix_web_sessions_expires_at", "web_sessions", ["expires_at"])
op.create_table(
"mcp_tokens",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("token_digest", sa.String(64), nullable=False, unique=True),
sa.Column("encrypted_token", sa.Text(), nullable=False),
sa.Column("access_mode", sa.String(16), nullable=False),
sa.Column("revoked", sa.Boolean(), nullable=False),
sa.Column("last_used_at", sa.DateTime(timezone=True)),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_mcp_tokens_token_digest", "mcp_tokens", ["token_digest"])
op.create_index("ix_mcp_tokens_revoked", "mcp_tokens", ["revoked"])
op.create_table(
"projects",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("slug", sa.String(200), nullable=False, unique=True),
sa.Column("git_remote", sa.Text(), unique=True),
sa.Column("archived", sa.Boolean(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_projects_slug", "projects", ["slug"])
op.create_index("ix_projects_archived", "projects", ["archived"])
op.create_table(
"project_aliases",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("project_id", sa.String(36), sa.ForeignKey("projects.id", ondelete="CASCADE")),
sa.Column("kind", sa.String(32), nullable=False),
sa.Column("value", sa.Text(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.UniqueConstraint("kind", "value", name="uq_project_alias_kind_value"),
)
op.create_index("ix_project_aliases_project_id", "project_aliases", ["project_id"])
op.create_table(
"memory_references",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("project_id", sa.String(36), sa.ForeignKey("projects.id", ondelete="SET NULL")),
sa.Column("scope", sa.String(16), nullable=False),
sa.Column("memory_type", sa.String(32), nullable=False),
sa.Column("path", sa.Text(), nullable=False, unique=True),
sa.Column("title", sa.String(300), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("tags_json", sa.Text(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_memory_references_project_id", "memory_references", ["project_id"])
op.create_index("ix_memory_references_scope", "memory_references", ["scope"])
op.create_index("ix_memory_references_memory_type", "memory_references", ["memory_type"])
op.create_index("ix_memory_references_status", "memory_references", ["status"])
op.create_index(
"ix_memory_scope_project_type",
"memory_references",
["scope", "project_id", "memory_type"],
)
op.create_table(
"idempotency_records",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("operation", sa.String(100), nullable=False),
sa.Column("request_id", sa.String(100), nullable=False),
sa.Column("response_json", sa.Text(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.UniqueConstraint("operation", "request_id", name="uq_idempotency_request"),
)
op.create_table(
"vault_connections",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("encrypted_server_url", sa.Text(), nullable=False),
sa.Column("encrypted_account", sa.Text(), nullable=False),
sa.Column("encrypted_password", sa.Text(), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("last_sync_at", sa.DateTime(timezone=True)),
sa.Column("last_error", sa.Text()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_table(
"credential_mappings",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("lookup_key", sa.String(300), nullable=False),
sa.Column("vault_item_id", sa.String(100), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.UniqueConstraint("lookup_key", name="uq_credential_mapping_lookup"),
)
op.create_index("ix_credential_mappings_lookup_key", "credential_mappings", ["lookup_key"])
op.create_table(
"file_records",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("project_id", sa.String(36), sa.ForeignKey("projects.id", ondelete="SET NULL")),
sa.Column("storage_path", sa.Text(), nullable=False, unique=True),
sa.Column("name", sa.String(300), nullable=False),
sa.Column("description", sa.Text()),
sa.Column("version", sa.String(100)),
sa.Column("purpose", sa.String(200)),
sa.Column("tags_json", sa.Text(), nullable=False),
sa.Column("mime_type", sa.String(200), nullable=False),
sa.Column("size", sa.Integer(), nullable=False),
sa.Column("checksum_sha256", sa.String(64), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("modified_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_file_records_project_id", "file_records", ["project_id"])
op.create_index("ix_file_records_name", "file_records", ["name"])
op.create_index("ix_file_records_version", "file_records", ["version"])
op.create_index("ix_file_records_purpose", "file_records", ["purpose"])
op.create_index("ix_file_records_checksum_sha256", "file_records", ["checksum_sha256"])
op.create_index("ix_file_records_status", "file_records", ["status"])
op.create_index("ix_file_project_status", "file_records", ["project_id", "status"])
op.create_table(
"upload_tasks",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("target_path", sa.Text(), nullable=False),
sa.Column("expected_size", sa.Integer(), nullable=False),
sa.Column("expected_sha256", sa.String(64)),
sa.Column("token_digest", sa.String(64), nullable=False, unique=True),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("error_message", sa.Text()),
sa.Column("file_id", sa.String(36), sa.ForeignKey("file_records.id", ondelete="SET NULL")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_upload_tasks_token_digest", "upload_tasks", ["token_digest"])
op.create_index("ix_upload_tasks_expires_at", "upload_tasks", ["expires_at"])
op.create_index("ix_upload_tasks_status", "upload_tasks", ["status"])
op.create_table(
"system_settings",
sa.Column("key", sa.String(100), primary_key=True),
sa.Column("value_json", sa.Text(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
def downgrade() -> None:
for table in (
"system_settings",
"upload_tasks",
"file_records",
"credential_mappings",
"vault_connections",
"idempotency_records",
"memory_references",
"project_aliases",
"projects",
"mcp_tokens",
"web_sessions",
"administrators",
):
op.drop_table(table)
@@ -0,0 +1,42 @@
"""Add upload task metadata."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260802_0002"
down_revision: str | Sequence[str] | None = "20260802_0001"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("upload_tasks") as batch:
batch.add_column(sa.Column("project_id", sa.String(36), nullable=True))
batch.add_column(sa.Column("description", sa.Text(), nullable=True))
batch.add_column(sa.Column("version", sa.String(100), nullable=True))
batch.add_column(sa.Column("purpose", sa.String(200), nullable=True))
batch.add_column(sa.Column("tags_json", sa.Text(), nullable=False, server_default="[]"))
batch.add_column(
sa.Column("overwrite_allowed", sa.Boolean(), nullable=False, server_default=sa.false())
)
batch.create_foreign_key(
"fk_upload_tasks_project_id", "projects", ["project_id"], ["id"], ondelete="SET NULL"
)
batch.create_index("ix_upload_tasks_project_id", ["project_id"])
def downgrade() -> None:
with op.batch_alter_table("upload_tasks") as batch:
batch.drop_index("ix_upload_tasks_project_id")
batch.drop_constraint("fk_upload_tasks_project_id", type_="foreignkey")
for column in (
"overwrite_allowed",
"tags_json",
"purpose",
"version",
"description",
"project_id",
):
batch.drop_column(column)
@@ -0,0 +1,30 @@
"""Add API-key authentication fields to vault connections."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260802_0003"
down_revision: str | Sequence[str] | None = "20260802_0002"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("vault_connections") as batch:
batch.add_column(
sa.Column(
"login_method",
sa.String(16),
nullable=False,
server_default="password",
)
)
batch.add_column(sa.Column("encrypted_client_secret", sa.Text(), nullable=True))
def downgrade() -> None:
with op.batch_alter_table("vault_connections") as batch:
batch.drop_column("encrypted_client_secret")
batch.drop_column("login_method")
@@ -0,0 +1,25 @@
"""Add directory records to the file catalog."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260802_0004"
down_revision: str | Sequence[str] | None = "20260802_0003"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("file_records") as batch:
batch.add_column(
sa.Column("is_directory", sa.Boolean(), nullable=False, server_default=sa.false())
)
batch.create_index("ix_file_records_is_directory", ["is_directory"])
def downgrade() -> None:
with op.batch_alter_table("file_records") as batch:
batch.drop_index("ix_file_records_is_directory")
batch.drop_column("is_directory")
@@ -0,0 +1,477 @@
"""Add AI curation and optional memory Git data model."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260804_0005"
down_revision: str | Sequence[str] | None = "20260802_0004"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"usage_profiles",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("name", sa.String(100), nullable=False, unique=True),
sa.Column("description", sa.Text()),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("is_shared", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_usage_profiles_enabled", "usage_profiles", ["enabled"])
with op.batch_alter_table("projects") as batch:
batch.add_column(
sa.Column(
"workspace_type",
sa.String(32),
nullable=False,
server_default="general",
)
)
batch.create_index("ix_projects_workspace_type", ["workspace_type"])
op.execute(
"UPDATE projects SET workspace_type = 'development' "
"WHERE git_remote IS NOT NULL AND TRIM(git_remote) <> ''"
)
with op.batch_alter_table("mcp_tokens") as batch:
batch.add_column(sa.Column("usage_profile_id", sa.String(36)))
batch.create_index("ix_mcp_tokens_usage_profile_id", ["usage_profile_id"])
batch.create_foreign_key(
"fk_mcp_tokens_usage_profile",
"usage_profiles",
["usage_profile_id"],
["id"],
ondelete="SET NULL",
)
with op.batch_alter_table("memory_references") as batch:
batch.add_column(sa.Column("usage_profile_id", sa.String(36)))
batch.create_index("ix_memory_references_usage_profile_id", ["usage_profile_id"])
batch.create_foreign_key(
"fk_memory_references_usage_profile",
"usage_profiles",
["usage_profile_id"],
["id"],
ondelete="SET NULL",
)
op.create_table(
"curation_settings",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("timezone", sa.String(100), nullable=False, server_default="Asia/Shanghai"),
sa.Column("context_input_tokens", sa.Integer(), nullable=False, server_default="32000"),
sa.Column("context_output_tokens", sa.Integer(), nullable=False, server_default="8000"),
sa.Column("task_max_calls", sa.Integer(), nullable=False, server_default="20"),
sa.Column("task_max_tokens", sa.Integer(), nullable=False, server_default="200000"),
sa.Column("batch_chars", sa.Integer(), nullable=False, server_default="80000"),
sa.Column("max_merge_levels", sa.Integer(), nullable=False, server_default="4"),
sa.Column("max_concurrency", sa.Integer(), nullable=False, server_default="1"),
sa.Column("source_freshness_hours", sa.Integer(), nullable=False, server_default="24"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_curation_settings_enabled", "curation_settings", ["enabled"])
op.create_table(
"curation_model_configs",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("revision", sa.Integer(), nullable=False, unique=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("base_url", sa.Text(), nullable=False),
sa.Column("encrypted_token", sa.Text()),
sa.Column("text_model", sa.String(300), nullable=False),
sa.Column("vision_model", sa.String(300)),
sa.Column("protocol", sa.String(32), nullable=False, server_default="auto"),
sa.Column("effective_protocol", sa.String(32)),
sa.Column("stream", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("timeout_seconds", sa.Integer(), nullable=False, server_default="120"),
sa.Column("probe_interval_seconds", sa.Integer(), nullable=False, server_default="300"),
sa.Column("management_url", sa.Text()),
sa.Column("capability_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_curation_model_configs_revision", "curation_model_configs", ["revision"])
op.create_index("ix_curation_model_configs_active", "curation_model_configs", ["active"])
op.create_table(
"curation_dependency_state",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("status", sa.String(32), nullable=False, server_default="unconfigured"),
sa.Column("capability_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column("last_check_at", sa.DateTime(timezone=True)),
sa.Column("last_success_at", sa.DateTime(timezone=True)),
sa.Column("last_failure_at", sa.DateTime(timezone=True)),
sa.Column("http_status", sa.Integer()),
sa.Column("error_code", sa.String(100)),
sa.Column("error_message", sa.Text()),
sa.Column("next_probe_at", sa.DateTime(timezone=True)),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_curation_dependency_state_status", "curation_dependency_state", ["status"])
op.create_index(
"ix_curation_dependency_state_next_probe_at",
"curation_dependency_state",
["next_probe_at"],
)
op.create_table(
"curation_schedules",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("trigger_type", sa.String(40), nullable=False),
sa.Column("scope", sa.String(16), nullable=False, server_default="project"),
sa.Column("project_id", sa.String(36), sa.ForeignKey("projects.id", ondelete="CASCADE")),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("inheritance", sa.String(20), nullable=False, server_default="instance"),
sa.Column("timezone", sa.String(100)),
sa.Column("recurrence_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column("missed_policy", sa.String(20), nullable=False, server_default="run_once"),
sa.Column("last_scheduled_at", sa.DateTime(timezone=True)),
sa.Column("last_triggered_at", sa.DateTime(timezone=True)),
sa.Column("next_run_at", sa.DateTime(timezone=True)),
sa.Column("status", sa.String(32), nullable=False, server_default="active"),
sa.Column("last_error", sa.Text()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_curation_schedules_trigger_type", "curation_schedules", ["trigger_type"])
op.create_index("ix_curation_schedules_project_id", "curation_schedules", ["project_id"])
op.create_index("ix_curation_schedules_enabled", "curation_schedules", ["enabled"])
op.create_index("ix_curation_schedules_next_run_at", "curation_schedules", ["next_run_at"])
op.create_table(
"curation_change_log",
sa.Column("sequence", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("scope", sa.String(16), nullable=False),
sa.Column("project_id", sa.String(36), sa.ForeignKey("projects.id", ondelete="SET NULL")),
sa.Column("source_type", sa.String(40), nullable=False),
sa.Column("source_id", sa.String(100), nullable=False),
sa.Column("change_type", sa.String(20), nullable=False),
sa.Column("revision", sa.String(100)),
sa.Column("content_hash", sa.String(64)),
sa.Column("target_hint", sa.String(200)),
sa.Column("summary", sa.Text()),
sa.Column(
"usage_profile_id",
sa.String(36),
sa.ForeignKey("usage_profiles.id", ondelete="SET NULL"),
),
sa.Column("origin", sa.String(20), nullable=False),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False),
)
for column in (
"scope",
"project_id",
"source_type",
"source_id",
"usage_profile_id",
"origin",
"observed_at",
):
op.create_index(f"ix_curation_change_log_{column}", "curation_change_log", [column])
op.create_table(
"curation_jobs",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("scope", sa.String(16), nullable=False),
sa.Column("project_id", sa.String(36), sa.ForeignKey("projects.id", ondelete="SET NULL")),
sa.Column(
"usage_profile_id",
sa.String(36),
sa.ForeignKey("usage_profiles.id", ondelete="SET NULL"),
),
sa.Column("mode", sa.String(20), nullable=False, server_default="incremental"),
sa.Column("trigger", sa.String(40), nullable=False),
sa.Column("source_strategy", sa.String(20), nullable=False, server_default="auto"),
sa.Column("target_documents_json", sa.Text(), nullable=False, server_default="[]"),
sa.Column("coalesce_key", sa.String(200)),
sa.Column("slot", sa.String(240), unique=True),
sa.Column("status", sa.String(32), nullable=False, server_default="queued"),
sa.Column("last_success_cursor", sa.Integer(), nullable=False, server_default="0"),
sa.Column("latest_observed_cursor", sa.Integer(), nullable=False, server_default="0"),
sa.Column("merged_trigger_count", sa.Integer(), nullable=False, server_default="1"),
sa.Column("trigger_summary_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column(
"model_config_id",
sa.String(36),
sa.ForeignKey("curation_model_configs.id", ondelete="SET NULL"),
),
sa.Column("prompt_version", sa.String(100)),
sa.Column("schema_version", sa.String(100)),
sa.Column("lease_owner", sa.String(100)),
sa.Column("lease_expires_at", sa.DateTime(timezone=True)),
sa.Column("retries", sa.Integer(), nullable=False, server_default="0"),
sa.Column("next_retry_at", sa.DateTime(timezone=True)),
sa.Column("error_code", sa.String(100)),
sa.Column("error_message", sa.Text()),
sa.Column("stats_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column("started_at", sa.DateTime(timezone=True)),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
for column in (
"scope",
"project_id",
"usage_profile_id",
"trigger",
"coalesce_key",
"status",
"lease_owner",
"lease_expires_at",
"next_retry_at",
):
op.create_index(f"ix_curation_jobs_{column}", "curation_jobs", [column])
op.create_table(
"curation_attempts",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column(
"job_id",
sa.String(36),
sa.ForeignKey("curation_jobs.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("attempt_number", sa.Integer(), nullable=False),
sa.Column(
"model_config_id",
sa.String(36),
sa.ForeignKey("curation_model_configs.id", ondelete="SET NULL"),
),
sa.Column("effective_protocol", sa.String(32)),
sa.Column("model", sa.String(300)),
sa.Column("prompt_version", sa.String(100), nullable=False, server_default="1"),
sa.Column("schema_version", sa.String(100), nullable=False, server_default="1"),
sa.Column("budget_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column("status", sa.String(32), nullable=False, server_default="running"),
sa.Column("error_code", sa.String(100)),
sa.Column("error_message", sa.Text()),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.UniqueConstraint("job_id", "attempt_number", name="uq_curation_attempt"),
)
op.create_index("ix_curation_attempts_job_id", "curation_attempts", ["job_id"])
op.create_table(
"curated_documents",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("scope", sa.String(16), nullable=False),
sa.Column("project_id", sa.String(36), sa.ForeignKey("projects.id", ondelete="CASCADE")),
sa.Column(
"usage_profile_id",
sa.String(36),
sa.ForeignKey("usage_profiles.id", ondelete="SET NULL"),
),
sa.Column("document_type", sa.String(100), nullable=False),
sa.Column("title", sa.String(300), nullable=False),
sa.Column("path", sa.Text(), nullable=False, unique=True),
sa.Column("revision", sa.Integer(), nullable=False, server_default="1"),
sa.Column(
"latest_job_id", sa.String(36), sa.ForeignKey("curation_jobs.id", ondelete="SET NULL")
),
sa.Column("source_hash", sa.String(64), nullable=False),
sa.Column("source_cursor", sa.Integer(), nullable=False, server_default="0"),
sa.Column("model", sa.String(300)),
sa.Column("prompt_version", sa.String(100), nullable=False, server_default="1"),
sa.Column("status", sa.String(32), nullable=False, server_default="active"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.UniqueConstraint(
"scope",
"project_id",
"usage_profile_id",
"document_type",
name="uq_curated_document",
),
)
for column in ("scope", "project_id", "usage_profile_id", "document_type", "status"):
op.create_index(f"ix_curated_documents_{column}", "curated_documents", [column])
op.create_table(
"curated_document_revisions",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column(
"document_id",
sa.String(36),
sa.ForeignKey("curated_documents.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("storage_path", sa.Text(), nullable=False, unique=True),
sa.Column("job_id", sa.String(36), sa.ForeignKey("curation_jobs.id", ondelete="SET NULL")),
sa.Column("model", sa.String(300)),
sa.Column("source_hash", sa.String(64), nullable=False),
sa.Column("change_summary", sa.Text()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.UniqueConstraint("document_id", "revision", name="uq_curated_document_revision"),
)
op.create_index(
"ix_curated_document_revisions_document_id",
"curated_document_revisions",
["document_id"],
)
op.create_index(
"ix_curated_document_revisions_job_id", "curated_document_revisions", ["job_id"]
)
op.create_table(
"curation_sources",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column(
"job_id",
sa.String(36),
sa.ForeignKey("curation_jobs.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("source_type", sa.String(40), nullable=False),
sa.Column("source_id", sa.String(100), nullable=False),
sa.Column("source_revision", sa.String(100)),
sa.Column("content_hash", sa.String(64)),
sa.Column("origin", sa.String(20), nullable=False),
sa.Column("sent_to_model", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("disposition", sa.String(20), nullable=False, server_default="processed"),
sa.Column("reason", sa.Text()),
sa.UniqueConstraint("job_id", "source_type", "source_id", name="uq_curation_source"),
)
op.create_index("ix_curation_sources_job_id", "curation_sources", ["job_id"])
op.create_table(
"curation_model_calls",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column(
"job_id",
sa.String(36),
sa.ForeignKey("curation_jobs.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column(
"attempt_id", sa.String(36), sa.ForeignKey("curation_attempts.id", ondelete="SET NULL")
),
sa.Column("purpose", sa.String(40), nullable=False),
sa.Column("protocol", sa.String(32), nullable=False),
sa.Column("stream", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("requested_model", sa.String(300), nullable=False),
sa.Column("response_model", sa.String(300)),
sa.Column("request_id", sa.String(300)),
sa.Column("prompt_version", sa.String(100), nullable=False),
sa.Column("input_tokens", sa.Integer()),
sa.Column("output_tokens", sa.Integer()),
sa.Column("latency_ms", sa.Integer()),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("error_code", sa.String(100)),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("finished_at", sa.DateTime(timezone=True)),
)
op.create_index("ix_curation_model_calls_job_id", "curation_model_calls", ["job_id"])
op.create_index("ix_curation_model_calls_attempt_id", "curation_model_calls", ["attempt_id"])
op.create_table(
"git_connections",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("mode", sa.String(20), nullable=False, server_default="single"),
sa.Column("remote_url", sa.Text()),
sa.Column("username", sa.String(500)),
sa.Column("transport", sa.String(20), nullable=False, server_default="https"),
sa.Column("credential_storage", sa.String(20), nullable=False, server_default="local"),
sa.Column("encrypted_credential", sa.Text()),
sa.Column("vault_item_id", sa.String(100)),
sa.Column("default_branch", sa.String(200), nullable=False, server_default="main"),
sa.Column("author_name", sa.String(200), nullable=False, server_default="MemRelay"),
sa.Column(
"author_email", sa.String(320), nullable=False, server_default="memrelay@localhost"
),
sa.Column("allow_write_test", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("allow_push_to_create", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("capability_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column("last_error", sa.Text()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_git_connections_enabled", "git_connections", ["enabled"])
op.create_table(
"memory_repositories",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column(
"connection_id", sa.String(36), sa.ForeignKey("git_connections.id", ondelete="SET NULL")
),
sa.Column("mode", sa.String(20), nullable=False),
sa.Column("scope", sa.String(16), nullable=False),
sa.Column("project_id", sa.String(36), sa.ForeignKey("projects.id", ondelete="SET NULL")),
sa.Column("local_path", sa.Text(), nullable=False, unique=True),
sa.Column("remote_url", sa.Text()),
sa.Column("branch", sa.String(200), nullable=False, server_default="main"),
sa.Column("status", sa.String(32), nullable=False, server_default="local"),
sa.Column("current_commit", sa.String(64)),
sa.Column("last_pushed_commit", sa.String(64)),
sa.Column("pending_commits", sa.Integer(), nullable=False, server_default="0"),
sa.Column("archived_at", sa.DateTime(timezone=True)),
sa.Column("last_error", sa.Text()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
for column in ("connection_id", "scope", "project_id", "status"):
op.create_index(f"ix_memory_repositories_{column}", "memory_repositories", [column])
op.create_table(
"git_sync_jobs",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("operation_id", sa.String(100), nullable=False, unique=True),
sa.Column(
"repository_id",
sa.String(36),
sa.ForeignKey("memory_repositories.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("resource_id", sa.String(100)),
sa.Column("action", sa.String(40), nullable=False),
sa.Column("summary", sa.String(500), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="pending"),
sa.Column("target_commit", sa.String(64)),
sa.Column("attempts", sa.Integer(), nullable=False, server_default="0"),
sa.Column("next_retry_at", sa.DateTime(timezone=True)),
sa.Column("error_message", sa.Text()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
for column in ("operation_id", "repository_id", "resource_id", "status", "next_retry_at"):
op.create_index(f"ix_git_sync_jobs_{column}", "git_sync_jobs", [column])
op.create_table(
"runtime_leases",
sa.Column("name", sa.String(100), primary_key=True),
sa.Column("owner", sa.String(100), nullable=False),
sa.Column("lease_expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index("ix_runtime_leases_owner", "runtime_leases", ["owner"])
op.create_index("ix_runtime_leases_lease_expires_at", "runtime_leases", ["lease_expires_at"])
def downgrade() -> None:
for table in (
"runtime_leases",
"git_sync_jobs",
"memory_repositories",
"git_connections",
"curation_model_calls",
"curation_sources",
"curated_document_revisions",
"curated_documents",
"curation_attempts",
"curation_jobs",
"curation_change_log",
"curation_schedules",
"curation_dependency_state",
"curation_model_configs",
"curation_settings",
):
op.drop_table(table)
with op.batch_alter_table("mcp_tokens") as batch:
batch.drop_constraint("fk_mcp_tokens_usage_profile", type_="foreignkey")
batch.drop_index("ix_mcp_tokens_usage_profile_id")
batch.drop_column("usage_profile_id")
with op.batch_alter_table("memory_references") as batch:
batch.drop_constraint("fk_memory_references_usage_profile", type_="foreignkey")
batch.drop_index("ix_memory_references_usage_profile_id")
batch.drop_column("usage_profile_id")
with op.batch_alter_table("projects") as batch:
batch.drop_index("ix_projects_workspace_type")
batch.drop_column("workspace_type")
op.drop_table("usage_profiles")
@@ -0,0 +1,95 @@
"""Add reliability leases, schedule deduplication, and profile context."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260804_0006"
down_revision: str | Sequence[str] | None = "20260804_0005"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("projects") as batch:
batch.add_column(sa.Column("custom_curation_template", sa.Text()))
with op.batch_alter_table("web_sessions") as batch:
batch.add_column(sa.Column("usage_profile_id", sa.String(36)))
batch.create_index("ix_web_sessions_usage_profile_id", ["usage_profile_id"])
batch.create_foreign_key(
"fk_web_sessions_usage_profile",
"usage_profiles",
["usage_profile_id"],
["id"],
ondelete="SET NULL",
)
with op.batch_alter_table("curation_attempts") as batch:
batch.add_column(sa.Column("attempt_key", sa.String(64)))
op.execute("UPDATE curation_attempts SET attempt_key = id WHERE attempt_key IS NULL")
with op.batch_alter_table("curation_attempts") as batch:
batch.alter_column("attempt_key", nullable=False)
batch.create_index("ix_curation_attempts_attempt_key", ["attempt_key"], unique=True)
with op.batch_alter_table("git_sync_jobs") as batch:
batch.add_column(sa.Column("lease_owner", sa.String(140)))
batch.add_column(sa.Column("lease_expires_at", sa.DateTime(timezone=True)))
batch.create_index("ix_git_sync_jobs_lease_owner", ["lease_owner"])
batch.create_index("ix_git_sync_jobs_lease_expires_at", ["lease_expires_at"])
op.create_table(
"curation_schedule_triggers",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column(
"schedule_id",
sa.String(36),
sa.ForeignKey("curation_schedules.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("trigger_key", sa.String(180), nullable=False, unique=True),
sa.Column("scheduled_for", sa.DateTime(timezone=True), nullable=False),
sa.Column("job_ids_json", sa.Text(), nullable=False, server_default="[]"),
sa.Column("status", sa.String(32), nullable=False, server_default="created"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index(
"ix_curation_schedule_triggers_schedule_id",
"curation_schedule_triggers",
["schedule_id"],
)
op.create_index(
"ix_curation_schedule_triggers_trigger_key",
"curation_schedule_triggers",
["trigger_key"],
unique=True,
)
op.create_index(
"ix_curation_schedule_triggers_scheduled_for",
"curation_schedule_triggers",
["scheduled_for"],
)
op.create_index(
"ix_curation_schedule_triggers_status",
"curation_schedule_triggers",
["status"],
)
def downgrade() -> None:
op.drop_table("curation_schedule_triggers")
with op.batch_alter_table("git_sync_jobs") as batch:
batch.drop_index("ix_git_sync_jobs_lease_expires_at")
batch.drop_index("ix_git_sync_jobs_lease_owner")
batch.drop_column("lease_expires_at")
batch.drop_column("lease_owner")
with op.batch_alter_table("curation_attempts") as batch:
batch.drop_index("ix_curation_attempts_attempt_key")
batch.drop_column("attempt_key")
with op.batch_alter_table("web_sessions") as batch:
batch.drop_constraint("fk_web_sessions_usage_profile", type_="foreignkey")
batch.drop_index("ix_web_sessions_usage_profile_id")
batch.drop_column("usage_profile_id")
with op.batch_alter_table("projects") as batch:
batch.drop_column("custom_curation_template")
@@ -0,0 +1,96 @@
"""Separate project identity from source checkout configuration."""
from collections.abc import Sequence
from urllib.parse import urlsplit
import sqlalchemy as sa
from alembic import op
revision: str = "20260804_0007"
down_revision: str | Sequence[str] | None = "20260804_0006"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _legacy_clone_url(value: str) -> str:
parsed = urlsplit(value)
if parsed.scheme == "git" and parsed.hostname and parsed.path:
port = f":{parsed.port}" if parsed.port else ""
return f"https://{parsed.hostname}{port}{parsed.path}.git"
return value
def upgrade() -> None:
with op.batch_alter_table("projects") as batch:
batch.add_column(sa.Column("git_remote_normalized", sa.Text()))
batch.add_column(
sa.Column("curation_enabled", sa.Boolean(), nullable=False, server_default=sa.true())
)
batch.add_column(
sa.Column("source_strategy", sa.String(20), nullable=False, server_default="auto")
)
batch.add_column(sa.Column("source_branch", sa.String(200)))
batch.add_column(sa.Column("source_credential_item_id", sa.String(100)))
batch.add_column(sa.Column("source_last_commit", sa.String(64)))
batch.add_column(sa.Column("source_last_branch", sa.String(200)))
batch.add_column(sa.Column("source_last_fetched_at", sa.DateTime(timezone=True)))
batch.add_column(sa.Column("source_last_error", sa.Text()))
batch.add_column(
sa.Column("source_cache_used", sa.Boolean(), nullable=False, server_default=sa.false())
)
batch.add_column(sa.Column("last_agent_sync_at", sa.DateTime(timezone=True)))
batch.add_column(sa.Column("last_agent_branch", sa.String(200)))
batch.add_column(sa.Column("last_agent_commit", sa.String(64)))
batch.add_column(sa.Column("last_agent_dirty", sa.Boolean()))
connection = op.get_bind()
projects = connection.execute(sa.text("SELECT id, git_remote FROM projects")).mappings()
for project in projects:
remote = project["git_remote"]
if not remote:
continue
connection.execute(
sa.text(
"UPDATE projects "
"SET git_remote = :clone_url, git_remote_normalized = :normalized "
"WHERE id = :project_id"
),
{
"clone_url": _legacy_clone_url(remote),
"normalized": remote,
"project_id": project["id"],
},
)
with op.batch_alter_table("projects") as batch:
batch.create_unique_constraint(
"uq_projects_git_remote_normalized", ["git_remote_normalized"]
)
batch.create_index("ix_projects_curation_enabled", ["curation_enabled"])
def downgrade() -> None:
connection = op.get_bind()
connection.execute(
sa.text(
"UPDATE projects SET git_remote = git_remote_normalized "
"WHERE git_remote_normalized IS NOT NULL"
)
)
with op.batch_alter_table("projects") as batch:
batch.drop_index("ix_projects_curation_enabled")
batch.drop_constraint("uq_projects_git_remote_normalized", type_="unique")
batch.drop_column("last_agent_dirty")
batch.drop_column("last_agent_commit")
batch.drop_column("last_agent_branch")
batch.drop_column("last_agent_sync_at")
batch.drop_column("source_cache_used")
batch.drop_column("source_last_error")
batch.drop_column("source_last_fetched_at")
batch.drop_column("source_last_branch")
batch.drop_column("source_last_commit")
batch.drop_column("source_credential_item_id")
batch.drop_column("source_branch")
batch.drop_column("source_strategy")
batch.drop_column("curation_enabled")
batch.drop_column("git_remote_normalized")
@@ -0,0 +1,29 @@
"""Persist curated document metadata for every revision."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260804_0008"
down_revision: str | Sequence[str] | None = "20260804_0007"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("curated_documents") as batch:
batch.add_column(
sa.Column("metadata_json", sa.Text(), nullable=False, server_default="{}")
)
with op.batch_alter_table("curated_document_revisions") as batch:
batch.add_column(
sa.Column("metadata_json", sa.Text(), nullable=False, server_default="{}")
)
def downgrade() -> None:
with op.batch_alter_table("curated_document_revisions") as batch:
batch.drop_column("metadata_json")
with op.batch_alter_table("curated_documents") as batch:
batch.drop_column("metadata_json")
@@ -0,0 +1,92 @@
"""Add inherited curation policies and configurable source limits."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260804_0009"
down_revision: str | Sequence[str] | None = "20260804_0008"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("curation_settings") as batch:
batch.add_column(
sa.Column("retry_initial_seconds", sa.Integer(), nullable=False, server_default="300")
)
batch.add_column(
sa.Column("retry_max_seconds", sa.Integer(), nullable=False, server_default="3600")
)
batch.add_column(
sa.Column("max_source_files", sa.Integer(), nullable=False, server_default="1000")
)
batch.add_column(
sa.Column(
"max_source_chars", sa.Integer(), nullable=False, server_default="20000000"
)
)
batch.add_column(
sa.Column(
"text_file_max_bytes", sa.Integer(), nullable=False, server_default="2097152"
)
)
batch.add_column(
sa.Column(
"rich_file_max_bytes", sa.Integer(), nullable=False, server_default="52428800"
)
)
op.create_table(
"curation_policy_overrides",
sa.Column("key", sa.String(length=120), nullable=False),
sa.Column("scope", sa.String(length=20), nullable=False),
sa.Column("workspace_type", sa.String(length=32), nullable=True),
sa.Column("project_id", sa.String(length=36), nullable=True),
sa.Column("values_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("key"),
)
op.create_index(
"ix_curation_policy_overrides_scope",
"curation_policy_overrides",
["scope"],
unique=False,
)
op.create_index(
"ix_curation_policy_overrides_workspace_type",
"curation_policy_overrides",
["workspace_type"],
unique=False,
)
op.create_index(
"ix_curation_policy_overrides_project_id",
"curation_policy_overrides",
["project_id"],
unique=False,
)
def downgrade() -> None:
op.drop_index(
"ix_curation_policy_overrides_project_id",
table_name="curation_policy_overrides",
)
op.drop_index(
"ix_curation_policy_overrides_workspace_type",
table_name="curation_policy_overrides",
)
op.drop_index(
"ix_curation_policy_overrides_scope",
table_name="curation_policy_overrides",
)
op.drop_table("curation_policy_overrides")
with op.batch_alter_table("curation_settings") as batch:
batch.drop_column("rich_file_max_bytes")
batch.drop_column("text_file_max_bytes")
batch.drop_column("max_source_chars")
batch.drop_column("max_source_files")
batch.drop_column("retry_max_seconds")
batch.drop_column("retry_initial_seconds")
@@ -0,0 +1,25 @@
"""Add all-profiles MCP token scope.
Revision ID: 20260805_0010
Revises: 20260804_0009
"""
import sqlalchemy as sa
from alembic import op
revision = "20260805_0010"
down_revision = "20260804_0009"
branch_labels = None
depends_on = None
def upgrade() -> None:
with op.batch_alter_table("mcp_tokens") as batch:
batch.add_column(
sa.Column("all_profiles", sa.Boolean(), nullable=False, server_default=sa.false())
)
def downgrade() -> None:
with op.batch_alter_table("mcp_tokens") as batch:
batch.drop_column("all_profiles")
@@ -0,0 +1,63 @@
"""Add bounded execution output events for curation jobs."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260806_0011"
down_revision: str | Sequence[str] | None = "20260805_0010"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"curation_execution_events",
sa.Column("sequence", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("job_id", sa.String(length=36), nullable=False),
sa.Column("attempt_id", sa.String(length=36), nullable=True),
sa.Column("phase", sa.String(length=32), nullable=False),
sa.Column("level", sa.String(length=16), nullable=False, server_default="info"),
sa.Column("message_code", sa.String(length=80), nullable=False),
sa.Column("details_json", sa.Text(), nullable=False, server_default="{}"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["attempt_id"], ["curation_attempts.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["job_id"], ["curation_jobs.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("sequence"),
)
op.create_index(
"ix_curation_execution_events_job_id",
"curation_execution_events",
["job_id"],
unique=False,
)
op.create_index(
"ix_curation_execution_events_attempt_id",
"curation_execution_events",
["attempt_id"],
unique=False,
)
op.create_index(
"ix_curation_execution_events_phase",
"curation_execution_events",
["phase"],
unique=False,
)
op.create_index(
"ix_curation_execution_events_job_created",
"curation_execution_events",
["job_id", "created_at"],
unique=False,
)
def downgrade() -> None:
op.drop_index(
"ix_curation_execution_events_job_created",
table_name="curation_execution_events",
)
op.drop_index("ix_curation_execution_events_phase", table_name="curation_execution_events")
op.drop_index("ix_curation_execution_events_attempt_id", table_name="curation_execution_events")
op.drop_index("ix_curation_execution_events_job_id", table_name="curation_execution_events")
op.drop_table("curation_execution_events")
@@ -0,0 +1,90 @@
"""Add derived memory curation lifecycle fields."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260806_0012"
down_revision: str | Sequence[str] | None = "20260806_0011"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("memory_references") as batch:
batch.add_column(
sa.Column(
"curation_status",
sa.String(length=32),
nullable=False,
server_default="pending",
)
)
batch.add_column(sa.Column("curated_revision", sa.Integer(), nullable=True))
batch.add_column(sa.Column("curated_at", sa.DateTime(timezone=True), nullable=True))
batch.add_column(
sa.Column("covered_by_json", sa.Text(), nullable=False, server_default="[]")
)
batch.add_column(
sa.Column("superseded_by_json", sa.Text(), nullable=False, server_default="[]")
)
batch.add_column(sa.Column("latest_curation_job_id", sa.String(length=36), nullable=True))
batch.create_index("ix_memory_references_curation_status", ["curation_status"])
op.execute(
"""
UPDATE curated_documents
SET status = 'superseded'
WHERE status = 'active'
AND id NOT IN (
SELECT id
FROM (
SELECT
id,
ROW_NUMBER() OVER (
PARTITION BY
scope,
COALESCE(project_id, ''),
COALESCE(usage_profile_id, ''),
document_type
ORDER BY updated_at DESC, revision DESC, id DESC
) AS position
FROM curated_documents
WHERE status = 'active'
) ranked
WHERE position = 1
)
"""
)
with op.batch_alter_table("curated_documents") as batch:
batch.drop_constraint("uq_curated_document", type_="unique")
op.execute(
"""
CREATE UNIQUE INDEX uq_active_curated_document
ON curated_documents (
scope,
COALESCE(project_id, ''),
COALESCE(usage_profile_id, ''),
document_type
)
WHERE status = 'active'
"""
)
def downgrade() -> None:
op.drop_index("uq_active_curated_document", table_name="curated_documents")
with op.batch_alter_table("curated_documents") as batch:
batch.create_unique_constraint(
"uq_curated_document",
["scope", "project_id", "usage_profile_id", "document_type"],
)
with op.batch_alter_table("memory_references") as batch:
batch.drop_index("ix_memory_references_curation_status")
batch.drop_column("latest_curation_job_id")
batch.drop_column("superseded_by_json")
batch.drop_column("covered_by_json")
batch.drop_column("curated_at")
batch.drop_column("curated_revision")
batch.drop_column("curation_status")
@@ -0,0 +1,35 @@
"""Persist upload activity provenance."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260806_0013"
down_revision: str | Sequence[str] | None = "20260806_0012"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("upload_tasks") as batch:
batch.add_column(sa.Column("usage_profile_id", sa.String(length=36), nullable=True))
batch.add_column(
sa.Column("origin", sa.String(length=20), nullable=False, server_default="user")
)
batch.create_index("ix_upload_tasks_usage_profile_id", ["usage_profile_id"])
batch.create_foreign_key(
"fk_upload_tasks_usage_profile_id",
"usage_profiles",
["usage_profile_id"],
["id"],
ondelete="SET NULL",
)
def downgrade() -> None:
with op.batch_alter_table("upload_tasks") as batch:
batch.drop_constraint("fk_upload_tasks_usage_profile_id", type_="foreignkey")
batch.drop_index("ix_upload_tasks_usage_profile_id")
batch.drop_column("origin")
batch.drop_column("usage_profile_id")
@@ -0,0 +1,71 @@
"""Cumulative curation coverage marks and model fallback candidates."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260808_0014"
down_revision: str | Sequence[str] | None = "20260806_0013"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("memory_references") as batch:
batch.add_column(sa.Column("covered_reason", sa.String(length=16), nullable=True))
batch.add_column(sa.Column("reviewed_revision", sa.Integer(), nullable=True))
batch.add_column(sa.Column("reviewed_job_id", sa.String(length=36), nullable=True))
with op.batch_alter_table("curation_model_configs") as batch:
batch.add_column(
sa.Column("candidates_json", sa.Text(), nullable=False, server_default="[]")
)
batch.add_column(
sa.Column(
"candidate_cooldown_seconds",
sa.Integer(),
nullable=False,
server_default="600",
)
)
op.create_table(
"curation_model_candidate_states",
sa.Column("model_name", sa.String(length=300), primary_key=True),
sa.Column("cooling_until", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_error_code", sa.String(length=100), nullable=True),
sa.Column("last_error_message", sa.Text(), nullable=True),
sa.Column("last_success_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_failure_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
op.create_index(
"ix_curation_model_candidate_states_cooling_until",
"curation_model_candidate_states",
["cooling_until"],
)
# Existing covered memories were all covered through citation; backfill the reason so
# the UI does not show them as unexplained.
op.execute(
"UPDATE memory_references SET covered_reason = 'cited' "
"WHERE curation_status = 'covered' AND covered_reason IS NULL"
)
def downgrade() -> None:
op.drop_index(
"ix_curation_model_candidate_states_cooling_until",
table_name="curation_model_candidate_states",
)
op.drop_table("curation_model_candidate_states")
with op.batch_alter_table("curation_model_configs") as batch:
batch.drop_column("candidate_cooldown_seconds")
batch.drop_column("candidates_json")
with op.batch_alter_table("memory_references") as batch:
batch.drop_column("reviewed_job_id")
batch.drop_column("reviewed_revision")
batch.drop_column("covered_reason")
@@ -0,0 +1,28 @@
"""Curation output language setting."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260812_0015"
down_revision: str | Sequence[str] | None = "20260808_0014"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("curation_settings") as batch:
batch.add_column(
sa.Column(
"output_language",
sa.String(length=16),
nullable=False,
server_default="auto",
)
)
def downgrade() -> None:
with op.batch_alter_table("curation_settings") as batch:
batch.drop_column("output_language")