mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
I mean, it's at a good place rn...
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user