Files
roboco/roboco/db/tables.py
T

923 lines
31 KiB
Python

"""
SQLAlchemy Table Definitions
ORM mappings for all RoboCo data models.
"""
from datetime import UTC, datetime, timedelta
from typing import Any
from uuid import UUID as PyUUID
from uuid import uuid4
from sqlalchemy import (
JSON,
Boolean,
DateTime,
Enum,
Float,
ForeignKey,
Index,
Integer,
Interval,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import ARRAY, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from roboco.db.base import Base
from roboco.models.base import (
AgentRole,
AgentStatus,
ChannelType,
Complexity,
HandoffStatus,
JournalEntryType,
MessageType,
NotificationPriority,
NotificationType,
SessionStatus,
TaskStatus,
Team,
)
from roboco.models.session import SessionScope
# =============================================================================
# AGENT TABLE
# =============================================================================
class AgentTable(Base):
"""SQLAlchemy table for agents."""
__tablename__ = "agents"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
slug: Mapped[str] = mapped_column(
String(50), unique=True, nullable=False, index=True
)
# Role & Team
role: Mapped[AgentRole] = mapped_column(Enum(AgentRole), nullable=False)
team: Mapped[Team | None] = mapped_column(Enum(Team), nullable=True)
# Status
status: Mapped[AgentStatus] = mapped_column(
Enum(AgentStatus), nullable=False, default=AgentStatus.OFFLINE
)
current_task_id: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True
)
# Configuration (stored as JSON)
model_config: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)
system_prompt: Mapped[str] = mapped_column(Text, nullable=False)
capabilities: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
# Permissions (stored as JSON)
permissions: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
# Metrics (stored as JSON)
metrics: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
# Journal
journal_id: Mapped[UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
# Description
description: Mapped[str | None] = mapped_column(Text, nullable=True)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
)
# Relationships - use lazy="joined" for single optional relationship
current_task: Mapped["TaskTable | None"] = relationship(
"TaskTable", foreign_keys=[current_task_id], lazy="joined"
)
# =============================================================================
# TASK TABLE
# =============================================================================
class TaskTable(Base):
"""SQLAlchemy table for tasks."""
__tablename__ = "tasks"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
title: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False)
acceptance_criteria: Mapped[list[str]] = mapped_column(
ARRAY(String), nullable=False
)
# Status
status: Mapped[TaskStatus] = mapped_column(
Enum(TaskStatus), nullable=False, default=TaskStatus.PENDING, index=True
)
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=2)
# Ownership
created_by: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False
)
assigned_to: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("agents.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
team: Mapped[Team] = mapped_column(Enum(Team), nullable=False, index=True)
# Relationships
parent_task_id: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True
)
dependency_ids: Mapped[list[PyUUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
blocker_ids: Mapped[list[PyUUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
)
claimed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
started_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
completed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
target_date: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Planning (stored as JSON)
plan: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
estimated_complexity: Mapped[Complexity] = mapped_column(
Enum(Complexity), nullable=False, default=Complexity.MEDIUM
)
# Execution (stored as JSON)
execution_log: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
checkpoints: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
progress_updates: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
# Artifacts (stored as JSON)
commits: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
documents: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
outputs: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
# Documentation
dev_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
qa_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
auditor_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
# Review Status
self_verified: Mapped[bool] = mapped_column(Boolean, default=False)
qa_verified: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
# Quick Context
quick_context: Mapped[str | None] = mapped_column(Text, nullable=True)
# Relationships
creator: Mapped["AgentTable"] = relationship(
"AgentTable", foreign_keys=[created_by], lazy="joined"
)
assignee: Mapped["AgentTable | None"] = relationship(
"AgentTable", foreign_keys=[assigned_to], lazy="joined"
)
parent_task: Mapped["TaskTable | None"] = relationship(
"TaskTable", remote_side=[id], lazy="select"
)
# Session links (many-to-many via SessionTaskTable)
session_links: Mapped[list["SessionTaskTable"]] = relationship(
"SessionTaskTable", back_populates="task", lazy="select"
)
__table_args__ = (
# Composite indexes for common queries
Index("ix_tasks_team_status", "team", "status"),
Index("ix_tasks_assigned_status", "assigned_to", "status"),
Index("ix_tasks_created_by_status", "created_by", "status"),
)
# =============================================================================
# CHANNEL TABLE
# =============================================================================
class ChannelTable(Base):
"""SQLAlchemy table for channels."""
__tablename__ = "channels"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
slug: Mapped[str] = mapped_column(
String(50), unique=True, nullable=False, index=True
)
type: Mapped[ChannelType] = mapped_column(Enum(ChannelType), nullable=False)
# Description
description: Mapped[str | None] = mapped_column(Text, nullable=True)
topic: Mapped[str | None] = mapped_column(String(500), nullable=True)
# Access Control
members: Mapped[list[PyUUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
writers: Mapped[list[PyUUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
silent_observers: Mapped[list[PyUUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
# Settings
is_archived: Mapped[bool] = mapped_column(Boolean, default=False)
is_private: Mapped[bool] = mapped_column(Boolean, default=False)
allow_threads: Mapped[bool] = mapped_column(Boolean, default=True)
allow_reactions: Mapped[bool] = mapped_column(Boolean, default=True)
message_retention_days: Mapped[int | None] = mapped_column(
Integer, nullable=True, default=90
)
max_message_length: Mapped[int] = mapped_column(Integer, default=10000)
# Statistics
message_count: Mapped[int] = mapped_column(Integer, default=0)
group_count: Mapped[int] = mapped_column(Integer, default=0)
last_activity: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
)
# Relationships - use lazy="select" for collections to avoid N+1
groups: Mapped[list["GroupTable"]] = relationship(
"GroupTable", back_populates="channel", lazy="select"
)
# =============================================================================
# GROUP TABLE
# =============================================================================
class GroupTable(Base):
"""SQLAlchemy table for groups."""
__tablename__ = "groups"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
channel_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("channels.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
# Access Control
allowed_roles: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
hierarchy_level: Mapped[int] = mapped_column(Integer, default=4)
members: Mapped[list[PyUUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
# Settings
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Current Session
active_session_id: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True), nullable=True
)
# Session Configuration (stored as JSON)
default_session_config: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
# Statistics
total_sessions: Mapped[int] = mapped_column(Integer, default=0)
total_messages: Mapped[int] = mapped_column(Integer, default=0)
last_activity: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
)
# Relationships - use lazy="select" for collections to avoid N+1
channel: Mapped["ChannelTable"] = relationship(
"ChannelTable", back_populates="groups"
)
sessions: Mapped[list["SessionTable"]] = relationship(
"SessionTable", back_populates="group", lazy="select"
)
# =============================================================================
# SESSION TABLE
# =============================================================================
class SessionTable(Base):
"""SQLAlchemy table for sessions."""
__tablename__ = "sessions"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
group_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("groups.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
# Boundaries
max_time_window: Mapped[timedelta | None] = mapped_column(
Interval, nullable=True, default=timedelta(minutes=30)
)
max_message_count: Mapped[int | None] = mapped_column(
Integer, nullable=True, default=100
)
max_content_length: Mapped[int | None] = mapped_column(
Integer, nullable=True, default=50000
)
# Timeout
timeout_seconds: Mapped[int] = mapped_column(Integer, default=300)
# State
status: Mapped[SessionStatus] = mapped_column(
Enum(SessionStatus), nullable=False, default=SessionStatus.ACTIVE, index=True
)
# Scope (for smart context loading)
scope: Mapped[SessionScope] = mapped_column(
Enum(SessionScope), nullable=False, default=SessionScope.TASK, index=True
)
# Timestamps
started_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
last_activity_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
closed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Statistics
message_count: Mapped[int] = mapped_column(Integer, default=0)
total_content_length: Mapped[int] = mapped_column(Integer, default=0)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
# Relationships - CRITICAL: use lazy="select" for messages (sessions can have 100+)
group: Mapped["GroupTable"] = relationship("GroupTable", back_populates="sessions")
messages: Mapped[list["MessageTable"]] = relationship(
"MessageTable", back_populates="session", lazy="select"
)
# Task links (many-to-many via SessionTaskTable)
task_links: Mapped[list["SessionTaskTable"]] = relationship(
"SessionTaskTable", back_populates="session", lazy="select"
)
__table_args__ = (
# Composite indexes for common queries
Index("ix_sessions_group_status", "group_id", "status"),
Index("ix_sessions_status_started", "status", "started_at"),
)
# =============================================================================
# SESSION-TASK JUNCTION TABLE
# =============================================================================
class SessionTaskTable(Base):
"""
Junction table for many-to-many Session ↔ Task relationship.
Enables PMs to create work sessions as discussion contexts for tasks.
A task can have multiple sessions (planning, review, retrospective).
A session can discuss multiple related tasks.
"""
__tablename__ = "session_tasks"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
# Foreign Keys (indexes defined in __table_args__)
session_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
)
task_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("tasks.id", ondelete="CASCADE"),
nullable=False,
)
# Relationship Metadata
is_primary: Mapped[bool] = mapped_column(
Boolean, default=False, nullable=False
) # Primary discussion session for this task
relationship_type: Mapped[str] = mapped_column(
String(50), default="discussion", nullable=False
) # "discussion", "planning", "review", "retrospective"
# Audit
added_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
added_by: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("agents.id", ondelete="SET NULL"),
nullable=True, # Allow NULL if PM is deleted
)
# Relationships
session: Mapped["SessionTable"] = relationship(
"SessionTable", back_populates="task_links", lazy="joined"
)
task: Mapped["TaskTable"] = relationship(
"TaskTable", back_populates="session_links", lazy="joined"
)
added_by_agent: Mapped["AgentTable | None"] = relationship(
"AgentTable", lazy="select"
)
__table_args__ = (
# Each session-task pair is unique
UniqueConstraint("session_id", "task_id", name="uq_session_task"),
# Partial unique index: only one primary session per task
Index(
"ix_session_tasks_primary_per_task",
"task_id",
unique=True,
postgresql_where=(is_primary.is_(True)),
),
# Fast lookups
Index("ix_session_tasks_task_id", "task_id"),
Index("ix_session_tasks_session_id", "session_id"),
Index("ix_session_tasks_type", "relationship_type"),
)
# =============================================================================
# MESSAGE TABLE
# =============================================================================
class MessageTable(Base):
"""SQLAlchemy table for messages."""
__tablename__ = "messages"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
# Source & Context
agent_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False, index=True
)
channel_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("channels.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
group_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("groups.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
session_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
# Content
type: Mapped[MessageType] = mapped_column(
Enum(MessageType), nullable=False, index=True
)
content: Mapped[str] = mapped_column(Text, nullable=False)
content_length: Mapped[int] = mapped_column(Integer, nullable=False)
# Threading
is_reply: Mapped[bool] = mapped_column(Boolean, default=False)
reply_to: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("messages.id", ondelete="SET NULL"),
nullable=True,
)
# Mentions
mentions: Mapped[list[UUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
# Task Context
task_id: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("tasks.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
commit_ref: Mapped[str | None] = mapped_column(String(40), nullable=True)
# Metadata
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False, index=True
)
# Extraction metadata
confidence: Mapped[float] = mapped_column(Float, default=1.0)
raw_excerpt: Mapped[str | None] = mapped_column(Text, nullable=True)
# Edit tracking (stored as JSON)
edited_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
edit_history: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
# Relationships - use lazy="joined" for agent to avoid N+1 on message lists
agent: Mapped["AgentTable"] = relationship("AgentTable", lazy="joined")
session: Mapped["SessionTable"] = relationship(
"SessionTable", back_populates="messages"
)
parent_message: Mapped["MessageTable | None"] = relationship(
"MessageTable", remote_side=[id], lazy="select"
)
__table_args__ = (
# Composite indexes for efficient queries
Index("ix_messages_channel_timestamp", "channel_id", "timestamp"),
Index("ix_messages_agent_timestamp", "agent_id", "timestamp"),
Index("ix_messages_session_timestamp", "session_id", "timestamp"),
)
# =============================================================================
# NOTIFICATION TABLE
# =============================================================================
class NotificationTable(Base):
"""SQLAlchemy table for notifications."""
__tablename__ = "notifications"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
type: Mapped[NotificationType] = mapped_column(
Enum(NotificationType), nullable=False, index=True
)
priority: Mapped[NotificationPriority] = mapped_column(
Enum(NotificationPriority), nullable=False, default=NotificationPriority.NORMAL
)
# Routing
from_agent: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False
)
to_agents: Mapped[list[UUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), nullable=False
)
# Content
subject: Mapped[str] = mapped_column(String(200), nullable=False)
body: Mapped[str] = mapped_column(Text, nullable=False)
# Acknowledgment
requires_ack: Mapped[bool] = mapped_column(Boolean, default=True)
acked_by: Mapped[list[PyUUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
acked_at: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
# Context
related_task_id: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True
)
related_message_ids: Mapped[list[PyUUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
# Timing
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False, index=True
)
expires_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Read tracking
read_by: Mapped[list[PyUUID]] = mapped_column(
ARRAY(UUID(as_uuid=True)), default=list
)
# Delivery tracking
delivered_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
# Relationships
sender: Mapped["AgentTable"] = relationship("AgentTable", lazy="joined")
related_task: Mapped["TaskTable | None"] = relationship("TaskTable", lazy="select")
__table_args__ = (
# Composite indexes for notification queries
Index("ix_notifications_from_agent_timestamp", "from_agent", "timestamp"),
Index("ix_notifications_type_priority", "type", "priority"),
Index("ix_notifications_timestamp_priority", "timestamp", "priority"),
)
# =============================================================================
# JOURNAL TABLE
# =============================================================================
class JournalTable(Base):
"""SQLAlchemy table for journals."""
__tablename__ = "journals"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
agent_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("agents.id", ondelete="CASCADE"),
nullable=False,
unique=True,
)
# Metadata
total_entries: Mapped[int] = mapped_column(Integer, default=0)
last_entry_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Summary
latest_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
summary_updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Growth metrics (stored as JSON)
entries_by_type: Mapped[dict[str, int]] = mapped_column(JSON, default=dict)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
)
# Relationships - use lazy="select" for entries collection to avoid N+1
agent: Mapped["AgentTable"] = relationship("AgentTable", lazy="joined")
entries: Mapped[list["JournalEntryTable"]] = relationship(
"JournalEntryTable", back_populates="journal", lazy="select"
)
class JournalEntryTable(Base):
"""SQLAlchemy table for journal entries."""
__tablename__ = "journal_entries"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
journal_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("journals.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
# Content
type: Mapped[JournalEntryType] = mapped_column(
Enum(JournalEntryType), nullable=False, index=True
)
title: Mapped[str] = mapped_column(String(200), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
# Context
task_id: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True
)
session_id: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("sessions.id", ondelete="SET NULL"),
nullable=True,
)
# Metadata
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False, index=True
)
tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
# Sentiment
sentiment: Mapped[str | None] = mapped_column(String(50), nullable=True)
# Visibility
is_private: Mapped[bool] = mapped_column(Boolean, default=False)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
)
# Relationships
journal: Mapped["JournalTable"] = relationship(
"JournalTable", back_populates="entries"
)
__table_args__ = (
# Composite indexes for journal entry queries
Index("ix_journal_entries_journal_timestamp", "journal_id", "timestamp"),
Index("ix_journal_entries_journal_type", "journal_id", "type"),
Index("ix_journal_entries_task_id", "task_id"),
)
# =============================================================================
# HANDOFF TABLE
# =============================================================================
class HandoffTable(Base):
"""SQLAlchemy table for documentation handoffs."""
__tablename__ = "handoffs"
# Identity
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
task_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("tasks.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
# Summary
summary: Mapped[str] = mapped_column(Text, nullable=False)
# What Changed (stored as JSON arrays)
new_functionality: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
modified_behavior: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
breaking_changes: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
# Documentation Needed (stored as JSON)
required_docs: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
optional_docs: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
# Key Commits (stored as JSON)
commits: Mapped[list[dict[str, str]]] = mapped_column(JSON, default=list)
# Code Locations (stored as JSON)
new_files: Mapped[list[dict[str, str]]] = mapped_column(JSON, default=list)
modified_files: Mapped[list[dict[str, str]]] = mapped_column(JSON, default=list)
# Conversations (stored as JSON)
key_conversations: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
# Code Samples (stored as JSON)
code_samples: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
# Gotchas (stored as JSON)
gotchas: Mapped[list[dict[str, str]]] = mapped_column(JSON, default=list)
# Related Documentation
related_docs: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
# Changelog Entry
changelog_entry: Mapped[str | None] = mapped_column(Text, nullable=True)
# Key Learnings (stored as JSON)
key_learnings: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
key_decisions: Mapped[list[dict[str, str]]] = mapped_column(JSON, default=list)
# Questions
questions: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
# Dev Notes Location
dev_notes_location: Mapped[str | None] = mapped_column(String(500), nullable=True)
# Status
status: Mapped[HandoffStatus] = mapped_column(
Enum(HandoffStatus), nullable=False, default=HandoffStatus.PENDING, index=True
)
assigned_to: Mapped[UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("agents.id", ondelete="SET NULL"), nullable=True
)
# Timestamps
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
)
updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
)
claimed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
completed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Documenter feedback
documenter_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
# Relationships
task: Mapped["TaskTable"] = relationship("TaskTable", lazy="joined")
assignee: Mapped["AgentTable | None"] = relationship("AgentTable", lazy="joined")
__table_args__ = (
UniqueConstraint("task_id", name="uq_handoffs_task_id"),
# Indexes for handoff queries
Index("ix_handoffs_assigned_status", "assigned_to", "status"),
Index("ix_handoffs_status_created", "status", "created_at"),
)