|
60 | 60 | from app.services.openclaw.gateway_dispatch import GatewayDispatchService |
61 | 61 | from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig |
62 | 62 | from app.services.openclaw.gateway_rpc import OpenClawGatewayError |
| 63 | +from app.services.openclaw.provisioning_db import AgentLifecycleService |
63 | 64 | from app.services.organizations import require_board_access |
64 | 65 | from app.services.tags import ( |
65 | 66 | TagState, |
@@ -661,15 +662,57 @@ async def _latest_task_comment_by_agent( |
661 | 662 | return (await session.exec(statement)).first() |
662 | 663 |
|
663 | 664 |
|
| 665 | +async def _wake_agent_online_for_task( |
| 666 | + *, |
| 667 | + session: AsyncSession, |
| 668 | + board: Board, |
| 669 | + task: Task, |
| 670 | + agent: Agent, |
| 671 | + reason: str, |
| 672 | +) -> None: |
| 673 | + if not agent.openclaw_session_id: |
| 674 | + return |
| 675 | + service = AgentLifecycleService(session) |
| 676 | + try: |
| 677 | + await service.commit_heartbeat(agent=agent, status_value="online") |
| 678 | + record_activity( |
| 679 | + session, |
| 680 | + event_type="task.assignee_woken", |
| 681 | + message=(f"Assignee heartbeat set online ({reason}): {agent.name}."), |
| 682 | + agent_id=agent.id, |
| 683 | + task_id=task.id, |
| 684 | + board_id=board.id, |
| 685 | + ) |
| 686 | + except Exception as exc: # pragma: no cover - best effort wake path |
| 687 | + record_activity( |
| 688 | + session, |
| 689 | + event_type="task.assignee_wake_failed", |
| 690 | + message=(f"Assignee wake failed ({reason}): {agent.name}. Error: {exc!s}"), |
| 691 | + agent_id=agent.id, |
| 692 | + task_id=task.id, |
| 693 | + board_id=board.id, |
| 694 | + ) |
| 695 | + await session.commit() |
| 696 | + |
| 697 | + |
664 | 698 | async def _notify_agent_on_task_assign( |
665 | 699 | *, |
666 | 700 | session: AsyncSession, |
667 | 701 | board: Board, |
668 | 702 | task: Task, |
669 | 703 | agent: Agent, |
| 704 | + wake_assignee: bool = True, |
670 | 705 | ) -> None: |
671 | 706 | if not agent.openclaw_session_id: |
672 | 707 | return |
| 708 | + if wake_assignee: |
| 709 | + await _wake_agent_online_for_task( |
| 710 | + session=session, |
| 711 | + board=board, |
| 712 | + task=task, |
| 713 | + agent=agent, |
| 714 | + reason="assignment", |
| 715 | + ) |
673 | 716 | dispatch = GatewayDispatchService(session) |
674 | 717 | config = await dispatch.optional_gateway_config_for_board(board) |
675 | 718 | if config is None: |
@@ -2121,15 +2164,23 @@ async def _lead_apply_status( |
2121 | 2164 | lead_agent = update.actor.agent |
2122 | 2165 | if "status" not in update.updates: |
2123 | 2166 | return |
| 2167 | + target_status = _required_status_value(update.updates["status"]) |
| 2168 | + # Leads may set `in_progress` when simultaneously assigning an agent to an |
| 2169 | + # inbox task (assignment-and-start shortcut). |
2124 | 2170 | if update.task.status != "review": |
| 2171 | + assigning_agent = "assigned_agent_id" in update.updates and bool( |
| 2172 | + _optional_assigned_agent_id(update.updates["assigned_agent_id"]) |
| 2173 | + ) |
| 2174 | + if update.task.status == "inbox" and target_status == "in_progress" and assigning_agent: |
| 2175 | + update.task.status = target_status |
| 2176 | + return |
2125 | 2177 | raise HTTPException( |
2126 | 2178 | status_code=status.HTTP_403_FORBIDDEN, |
2127 | 2179 | detail=( |
2128 | 2180 | "Lead status gate failed: board leads can only change status when the current " |
2129 | 2181 | f"task status is `review` (current: `{update.task.status}`)." |
2130 | 2182 | ), |
2131 | 2183 | ) |
2132 | | - target_status = _required_status_value(update.updates["status"]) |
2133 | 2184 | if target_status not in {"done", "inbox"}: |
2134 | 2185 | raise HTTPException( |
2135 | 2186 | status_code=status.HTTP_403_FORBIDDEN, |
@@ -2521,66 +2572,88 @@ async def _notify_task_update_assignment_changes( |
2521 | 2572 | *, |
2522 | 2573 | update: _TaskUpdateInput, |
2523 | 2574 | ) -> None: |
| 2575 | + board: Board | None = None |
| 2576 | + |
| 2577 | + async def _board() -> Board | None: |
| 2578 | + nonlocal board |
| 2579 | + if board is None and update.task.board_id: |
| 2580 | + board = await Board.objects.by_id(update.task.board_id).first(session) |
| 2581 | + return board |
| 2582 | + |
2524 | 2583 | if ( |
2525 | 2584 | update.task.status == "inbox" |
2526 | 2585 | and update.task.assigned_agent_id is None |
2527 | 2586 | and (update.previous_status != "inbox" or update.previous_assigned is not None) |
2528 | 2587 | ): |
2529 | | - board = ( |
2530 | | - await Board.objects.by_id(update.task.board_id).first(session) |
2531 | | - if update.task.board_id |
2532 | | - else None |
2533 | | - ) |
2534 | | - if board: |
| 2588 | + current_board = await _board() |
| 2589 | + if current_board: |
2535 | 2590 | await _notify_lead_on_task_unassigned( |
2536 | 2591 | session=session, |
2537 | | - board=board, |
| 2592 | + board=current_board, |
2538 | 2593 | task=update.task, |
2539 | 2594 | ) |
2540 | 2595 |
|
2541 | | - if ( |
2542 | | - not update.task.assigned_agent_id |
2543 | | - or update.task.assigned_agent_id == update.previous_assigned |
2544 | | - ): |
| 2596 | + if not update.task.assigned_agent_id: |
2545 | 2597 | return |
| 2598 | + |
2546 | 2599 | assigned_agent = await Agent.objects.by_id(update.task.assigned_agent_id).first( |
2547 | 2600 | session, |
2548 | 2601 | ) |
2549 | 2602 | if assigned_agent is None: |
2550 | 2603 | return |
2551 | | - board = ( |
2552 | | - await Board.objects.by_id(update.task.board_id).first(session) |
2553 | | - if update.task.board_id |
2554 | | - else None |
| 2604 | + |
| 2605 | + assignment_changed = update.task.assigned_agent_id != update.previous_assigned |
| 2606 | + entered_in_progress = ( |
| 2607 | + update.task.status == "in_progress" and update.previous_status != "in_progress" |
2555 | 2608 | ) |
| 2609 | + |
| 2610 | + if entered_in_progress and not assignment_changed: |
| 2611 | + current_board = await _board() |
| 2612 | + if current_board: |
| 2613 | + await _wake_agent_online_for_task( |
| 2614 | + session=session, |
| 2615 | + board=current_board, |
| 2616 | + task=update.task, |
| 2617 | + agent=assigned_agent, |
| 2618 | + reason="status_in_progress", |
| 2619 | + ) |
| 2620 | + |
| 2621 | + if not assignment_changed: |
| 2622 | + return |
| 2623 | + |
2556 | 2624 | if ( |
2557 | 2625 | update.previous_status == "review" |
2558 | 2626 | and update.task.status == "inbox" |
2559 | 2627 | and update.actor.actor_type == "agent" |
2560 | 2628 | and update.actor.agent |
2561 | 2629 | and update.actor.agent.is_board_lead |
2562 | 2630 | ): |
2563 | | - if board: |
| 2631 | + current_board = await _board() |
| 2632 | + if current_board: |
2564 | 2633 | await _notify_agent_on_task_rework( |
2565 | 2634 | session=session, |
2566 | | - board=board, |
| 2635 | + board=current_board, |
2567 | 2636 | task=update.task, |
2568 | 2637 | agent=assigned_agent, |
2569 | 2638 | lead=update.actor.agent, |
2570 | 2639 | ) |
2571 | 2640 | return |
| 2641 | + |
2572 | 2642 | if ( |
2573 | 2643 | update.actor.actor_type == "agent" |
2574 | 2644 | and update.actor.agent |
2575 | 2645 | and update.task.assigned_agent_id == update.actor.agent.id |
2576 | 2646 | ): |
2577 | 2647 | return |
2578 | | - if board: |
| 2648 | + |
| 2649 | + current_board = await _board() |
| 2650 | + if current_board: |
2579 | 2651 | await _notify_agent_on_task_assign( |
2580 | 2652 | session=session, |
2581 | | - board=board, |
| 2653 | + board=current_board, |
2582 | 2654 | task=update.task, |
2583 | 2655 | agent=assigned_agent, |
| 2656 | + wake_assignee=True, |
2584 | 2657 | ) |
2585 | 2658 |
|
2586 | 2659 |
|
|
0 commit comments