I mean, it's at a good place rn...

This commit is contained in:
Renn F
2026-04-20 15:10:54 +02:00
parent 0023c25d60
commit 8e201901c0
264 changed files with 36484 additions and 748 deletions
+116 -89
View File
@@ -9,14 +9,15 @@ Adds:
orchestrator restarts don't strand them (previously in-memory dict).
- `audit_log`: queryable audit trail so the auditor role actually has data
to inspect (previously log-only).
- `notificationtype` enum value `approval`: the orchestrator's approval
- `notificationtype` enum value `APPROVAL`: the orchestrator's approval
dispatcher and the frontend both reference this type, but it was missing
from the enum, so every `/notifications?type_filter=approval` call
returned 422 and the dispatcher's query silently matched nothing.
from the enum, so every approval-related insert raised
`invalid input value for enum notificationtype: "APPROVAL"`.
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy import inspect
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
@@ -26,98 +27,124 @@ branch_labels = None
depends_on = None
def _table_exists(name: str) -> bool:
"""True if `name` already exists in the current DB schema.
Guards against re-running this migration on DBs that were first created
via Base.metadata.create_all (which pre-created waiting_records and
audit_log from the ORM metadata). Without this guard, op.create_table
raises DuplicateTableError and the whole migration rolls back — so the
ALTER TYPE ADD VALUE 'APPROVAL' above it never takes effect either.
"""
bind = op.get_bind()
return inspect(bind).has_table(name)
def upgrade() -> None:
# Add the missing 'approval' value to the notificationtype enum.
# Add the missing APPROVAL value to the notificationtype enum.
# SQLAlchemy's default Enum(PyEnum) binds enum members by NAME (uppercase),
# and the live DB was created via Base.metadata.create_all, so the enum
# values on disk are the uppercase NAMES ('TASK_ASSIGNMENT', 'ALERT', ...).
# IF NOT EXISTS makes this idempotent on re-runs.
op.execute(
"ALTER TYPE notificationtype ADD VALUE IF NOT EXISTS 'approval'"
"ALTER TYPE notificationtype ADD VALUE IF NOT EXISTS 'APPROVAL'"
)
op.create_table(
"waiting_records",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"agent_id",
sa.String(64),
nullable=False,
unique=True,
index=True,
comment="Agent slug; unique so only one record per agent.",
),
sa.Column(
"task_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("tasks.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column(
"waiting_for",
sa.String(64),
nullable=False,
comment="One of: blocker_resolution, qa_result, answer, assignment",
),
sa.Column(
"waiting_since",
sa.DateTime(timezone=True),
nullable=False,
),
sa.Column("context", postgresql.JSONB, nullable=False, server_default="{}"),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
)
op.create_index(
"ix_waiting_records_waiting_for", "waiting_records", ["waiting_for"]
)
if not _table_exists("waiting_records"):
op.create_table(
"waiting_records",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"agent_id",
sa.String(64),
nullable=False,
unique=True,
index=True,
comment="Agent slug; unique so only one record per agent.",
),
sa.Column(
"task_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("tasks.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column(
"waiting_for",
sa.String(64),
nullable=False,
comment="One of: blocker_resolution, qa_result, answer, assignment",
),
sa.Column(
"waiting_since",
sa.DateTime(timezone=True),
nullable=False,
),
sa.Column(
"context", postgresql.JSONB, nullable=False, server_default="{}"
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
)
op.create_index(
"ix_waiting_records_waiting_for",
"waiting_records",
["waiting_for"],
)
op.create_table(
"audit_log",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"event_type",
sa.String(80),
nullable=False,
index=True,
comment="Dot-separated e.g. task.claimed, session.closed, project.deleted",
),
sa.Column(
"agent_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("agents.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column(
"target_type",
sa.String(40),
nullable=True,
comment="e.g. task, session, project, notification",
),
sa.Column("target_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column(
"severity",
sa.String(16),
nullable=False,
server_default="info",
comment="info | warning | error",
),
sa.Column("details", postgresql.JSONB, nullable=False, server_default="{}"),
sa.Column(
"timestamp",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
index=True,
),
)
op.create_index(
"ix_audit_log_agent_timestamp", "audit_log", ["agent_id", "timestamp"]
)
op.create_index(
"ix_audit_log_target", "audit_log", ["target_type", "target_id"]
)
if not _table_exists("audit_log"):
op.create_table(
"audit_log",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"event_type",
sa.String(80),
nullable=False,
index=True,
comment="Dot-separated e.g. task.claimed, session.closed, project.deleted",
),
sa.Column(
"agent_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("agents.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column(
"target_type",
sa.String(40),
nullable=True,
comment="e.g. task, session, project, notification",
),
sa.Column("target_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column(
"severity",
sa.String(16),
nullable=False,
server_default="info",
comment="info | warning | error",
),
sa.Column(
"details", postgresql.JSONB, nullable=False, server_default="{}"
),
sa.Column(
"timestamp",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
index=True,
),
)
op.create_index(
"ix_audit_log_agent_timestamp",
"audit_log",
["agent_id", "timestamp"],
)
op.create_index(
"ix_audit_log_target", "audit_log", ["target_type", "target_id"]
)
def downgrade() -> None:
@@ -0,0 +1,48 @@
"""Add blocker_resolver_type to tasks.
Revision ID: 003_blocker_resolver_type
Revises: 002_persistence_tables
Create Date: 2026-04-19
Adds `tasks.blocker_resolver_type` so the dispatcher can tell the difference
between blocks an agent can resolve (dispatcher may respawn) and blocks that
need human intervention (dispatcher must NOT respawn). Without this, agents
keep churning on HITL-blocked tasks and burning tokens.
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "003_blocker_resolver_type"
down_revision = "002_persistence_tables"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create the enum type — names match SA's default Enum(PyEnum) binding
# (it serializes enum members by NAME, which is uppercase per PEP 8).
blocker_resolver_enum = sa.Enum(
"AGENT",
"HUMAN",
name="blockerresolvertype",
)
blocker_resolver_enum.create(op.get_bind(), checkfirst=True)
# Add nullable column — NULL means "not applicable" (task isn't blocked)
# or "legacy block before this migration". Dispatcher treats NULL the
# same as AGENT (old default behavior) to preserve back-compat.
op.add_column(
"tasks",
sa.Column(
"blocker_resolver_type",
blocker_resolver_enum,
nullable=True,
),
)
def downgrade() -> None:
op.drop_column("tasks", "blocker_resolver_type")
sa.Enum(name="blockerresolvertype").drop(op.get_bind(), checkfirst=True)