diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index 70cd8c2a..29a810b0 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -2137,8 +2137,10 @@ async def _apply_non_lead_agent_task_rules( raise _blocked_task_error(blocked_ids) if status_value == "inbox": update.task.assigned_agent_id = None + update.task.previous_in_progress_at = update.task.in_progress_at update.task.in_progress_at = None elif status_value == "review": + update.task.previous_in_progress_at = update.task.in_progress_at update.task.assigned_agent_id = None update.task.in_progress_at = None else: @@ -2199,8 +2201,12 @@ async def _apply_admin_task_rules( if "status" in update.updates: status_value = _required_status_value(update.updates["status"]) if status_value == "inbox": + update.task.previous_in_progress_at = update.task.in_progress_at update.task.assigned_agent_id = None update.task.in_progress_at = None + elif status_value == "review": + update.task.previous_in_progress_at = update.task.in_progress_at + update.task.in_progress_at = None elif status_value == "in_progress": update.task.in_progress_at = utcnow() @@ -2353,8 +2359,8 @@ async def _finalize_updated_task( comment_text = (update.comment or "").strip() review_comment_author = update.task.assigned_agent_id or update.previous_assigned review_comment_since = ( - update.task.in_progress_at - if update.task.in_progress_at is not None + update.task.previous_in_progress_at + if update.task.previous_in_progress_at is not None else update.previous_in_progress_at ) if not comment_text and not await has_valid_recent_comment( diff --git a/backend/app/models/tasks.py b/backend/app/models/tasks.py index 7ffab7c8..eea5f812 100644 --- a/backend/app/models/tasks.py +++ b/backend/app/models/tasks.py @@ -27,6 +27,7 @@ class Task(TenantScoped, table=True): priority: str = Field(default="medium", index=True) due_at: datetime | None = None in_progress_at: datetime | None = None + previous_in_progress_at: datetime | None = None created_by_user_id: UUID | None = Field( default=None, diff --git a/backend/migrations/versions/a2f6c9d4b7e8_add_previous_in_progress_at_to_tasks.py b/backend/migrations/versions/a2f6c9d4b7e8_add_previous_in_progress_at_to_tasks.py new file mode 100644 index 00000000..0eb25dfe --- /dev/null +++ b/backend/migrations/versions/a2f6c9d4b7e8_add_previous_in_progress_at_to_tasks.py @@ -0,0 +1,31 @@ +"""Track prior in_progress_at during status transitions. + +Revision ID: a2f6c9d4b7e8 +Revises: 4c1f5e2a7b9d +Create Date: 2026-02-15 00:00:00.000000 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "a2f6c9d4b7e8" +down_revision = "4c1f5e2a7b9d" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Add previous_in_progress_at column to tasks.""" + op.add_column( + "tasks", + sa.Column("previous_in_progress_at", sa.DateTime(), nullable=True), + ) + + +def downgrade() -> None: + """Drop previous_in_progress_at column from tasks.""" + op.drop_column("tasks", "previous_in_progress_at") diff --git a/backend/tests/test_task_agent_permissions.py b/backend/tests/test_task_agent_permissions.py index dd0e02ab..e4d0f377 100644 --- a/backend/tests/test_task_agent_permissions.py +++ b/backend/tests/test_task_agent_permissions.py @@ -340,6 +340,7 @@ async def test_non_lead_agent_moves_task_to_review_and_task_unassigns() -> None: gateway_id = uuid4() worker_id = uuid4() task_id = uuid4() + in_progress_at = utcnow() session.add(Organization(id=org_id, name="org")) session.add( @@ -377,7 +378,7 @@ async def test_non_lead_agent_moves_task_to_review_and_task_unassigns() -> None: description="", status="in_progress", assigned_agent_id=worker_id, - in_progress_at=utcnow(), + in_progress_at=in_progress_at, ), ) await session.commit() @@ -397,6 +398,12 @@ async def test_non_lead_agent_moves_task_to_review_and_task_unassigns() -> None: assert updated.status == "review" assert updated.assigned_agent_id is None assert updated.in_progress_at is None + + refreshed_task = ( + (await session.exec(select(Task).where(col(Task.id) == task_id))).first() + ) + assert refreshed_task is not None + assert refreshed_task.previous_in_progress_at == in_progress_at finally: await engine.dispose()