Skip to content

Conversation

@qdaxb
Copy link
Contributor

@qdaxb qdaxb commented Nov 18, 2025

Summary

Implements task cancellation functionality allowing users to stop running tasks via the Web UI without deleting Docker containers or closing Agent connections. This enables users to continue conversations in the same session after canceling a task.

Changes

Backend API Layer

  • tasks.py: Added POST /api/tasks/{task_id}/cancel endpoint
    • Validates user owns the task
    • Only allows canceling RUNNING or PENDING tasks
    • Returns updated task information

Service Layer

  • task_kinds.py: Implemented cancel_task() method

    • Updates task status to CANCELLED using optimistic locking
    • Updates all associated subtasks to CANCELLED
    • Notifies executors to stop tasks (best effort)
    • Does NOT delete Docker containers or Agent connections
  • executor_kinds.py: Added stop executor methods

    • stop_executor_task_sync(): Synchronous version
    • stop_executor_task_async(): Asynchronous version
    • Sends HTTP requests to executor manager stop endpoint

Configuration

  • config.py: Added EXECUTOR_STOP_TASK_URL configuration
    • Default: http://localhost:8001/executor-manager/executor/stop

Executor Manager Layer

  • routers.py: Added POST /executor-manager/executor/stop route
    • Receives executor name and namespace
    • Delegates to executor's stop_task() method

Executor Implementation

  • base.py: Added stop_task() abstract method in Executor base class

  • docker/executor.py: Implemented stop_task() for Docker executor

    • Validates container ownership
    • Gets container port information
    • Sends HTTP POST to container's /stop endpoint
    • Handles timeouts gracefully (acceptable behavior)

Key Features

  1. Non-destructive cancellation: Stops task execution without deleting resources
  2. Session continuity: Users can continue conversations in the same session
  3. Optimistic locking: Prevents race conditions during status updates
  4. Best effort notification: Attempts to notify executors but doesn't fail if notification fails
  5. Proper status tracking: Updates both task and subtask statuses to CANCELLED

Testing Notes

  • Only RUNNING or PENDING tasks can be cancelled
  • Returns 400 error for tasks in other states (COMPLETED, FAILED, etc.)
  • Returns 404 error if task doesn't exist or doesn't belong to user
  • Docker containers remain running and accessible after cancellation

Summary by CodeRabbit

Release Notes

  • New Features
    • Added task cancellation capability—users can now stop running or pending tasks via a new Stop button in the task interface
    • Task cancellation provides real-time feedback with success and failure messages
    • Full localization support for English and Chinese

This commit implements the ability to cancel running tasks via the Web UI stop button.
The cancellation stops task execution without deleting Docker containers or closing Agent connections,
allowing users to continue conversations in the same session.

Backend changes:
- Add cancel_task API endpoint (POST /api/tasks/{task_id}/cancel) in tasks.py
- Implement cancel_task business logic in TaskKindsService with optimistic locking
- Add stop_executor_task methods (sync/async) in ExecutorKindsService
- Add EXECUTOR_STOP_TASK_URL configuration in config.py

Executor Manager changes:
- Add stop_task abstract method in Executor base class
- Implement stop_task in DockerExecutor to send HTTP stop signal to containers
- Add /executor-manager/executor/stop route in routers.py

Key features:
- Updates task and subtask statuses to CANCELLED
- Sends stop signals to running executors without deleting containers
- Maintains session continuity for user to continue conversations
- Uses optimistic locking to prevent race conditions
@coderabbitai
Copy link

coderabbitai bot commented Nov 18, 2025

Walkthrough

A task cancellation feature is implemented across the full stack: backend API endpoint, service logic to update task status and notify executors, executor stop capability, and frontend UI with translated stop button and success/error messaging.

Changes

Cohort / File(s) Summary
Backend API Endpoint
backend/app/api/endpoints/adapter/tasks.py
Adds cancel_task POST endpoint at /{task_id}/cancel that returns TaskInDB and delegates to task service cancellation.
Backend Configuration
backend/app/core/config.py
Introduces EXECUTOR_STOP_TASK_URL configuration attribute with default pointing to executor manager stop endpoint.
Backend Task Service
backend/app/services/adapters/task_kinds.py
Implements cancel_task method that updates task status to CANCELLED with optimistic locking, cancels subtasks, collects unique running executors, and calls stop_executor_task_sync for each to signal container stop; includes error handling for not-found and invalid status.
Backend Executor Service
backend/app/services/adapters/executor_kinds.py
Adds stop_executor_task_sync and stop_executor_task_async methods to post to EXECUTOR_STOP_TASK_URL with executor validation, JSON response handling, and error paths (HTTP 400 for missing executor_name, HTTP 500 for request failures).
Executor Manager Base Interface
executor_manager/executors/base.py
Adds abstract method stop_task(executor_name: str) -> Dict[str, Any] to Executor interface.
Executor Manager Docker Implementation
executor_manager/executors/docker/executor.py
Implements stop_task to validate container ownership, retrieve container port, POST to container's /stop endpoint, and handle outcomes (success/unauthorized/failed/partial/timeout) with timeout and exception handling.
Executor Manager API Router
executor_manager/routers/routers.py
Adds StopExecutorRequest model and new /executor-manager/executor/stop endpoint that logs requests, dispatches to executor's stop_task method, and returns result or HTTP 500 on error.
Frontend API Client
frontend/src/apis/tasks.ts
Adds cancelTask(id: number): Promise<Task> method that POSTs to /tasks/{id}/cancel.
Frontend Task Context
frontend/src/features/tasks/contexts/taskContext.tsx
Introduces cancelTask(taskId: number): Promise<void> method and isCancelling: boolean state to TaskContextType; cancellation updates task list, refreshes selected task detail when applicable, and resets state on completion or error.
Frontend UI Chat Area
frontend/src/features/tasks/components/ChatArea.tsx
Imports StopIcon, adds translation hook, implements handleStopTask handler, and conditionally renders Stop button (with spinner when cancelling) for RUNNING/PENDING tasks; shows success/failure messages via i18n.
Frontend i18n Translations
frontend/src/i18n/locales/en/common.json, frontend/src/i18n/locales/zh-CN/common.json
Adds new "task" namespace with three keys: stop ("Stop Task" / "停止任务"), cancel_success ("Task cancelled successfully" / "任务已成功取消"), cancel_failed ("Failed to cancel task" / "取消任务失败").

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend UI
    participant TaskContext
    participant Backend API
    participant TaskService
    participant ExecutorService
    participant ExecutorManager
    participant Container

    User->>Frontend UI: Click Stop Button
    Frontend UI->>TaskContext: cancelTask(taskId)
    TaskContext->>Backend API: POST /tasks/{id}/cancel
    Backend API->>TaskService: cancel_task(db, task_id, user_id)
    
    TaskService->>TaskService: Update task status to CANCELLED
    TaskService->>TaskService: Cancel all subtasks
    TaskService->>TaskService: Collect unique running executors
    
    loop For each executor
        TaskService->>ExecutorService: stop_executor_task_sync(executor)
        ExecutorService->>ExecutorManager: POST /executor-manager/executor/stop
        ExecutorManager->>ExecutorManager: Dispatch to executor.stop_task()
        ExecutorManager->>Container: POST /stop
        Container-->>ExecutorManager: Stop signal received
        ExecutorManager-->>ExecutorService: Success response
    end
    
    TaskService-->>Backend API: Return updated task (CANCELLED)
    Backend API-->>TaskContext: TaskInDB response
    TaskContext->>TaskContext: Update state + show success message
    TaskContext-->>Frontend UI: Refresh task details
    Frontend UI-->>User: Display cancelled task state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Key areas requiring attention:
    • cancel_task logic in TaskKindsService — verify optimistic locking correctness, subtask cancellation scope, and executor notification completeness
    • stop_task in DockerExecutor — HTTP call to container endpoint, timeout handling, and broad exception coverage
    • Error propagation across API → service → executor layers for consistency
    • Frontend context state management (isCancelling) and side-effect ordering (task update before refresh)
    • Async/sync method pair implementations in executor_kinds.py for parity

Poem

🐰 A hop, skip, and stop command,
Tasks now pause at our demand!
Containers freeze mid-flight with grace,
Stop buttons shine in every place. ✨
The rabbit cheers—control at hand!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the main feature being added: task cancellation functionality triggered by a Web UI stop button.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch wegent/task-cancel-feature

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Implemented stop button in Web UI to cancel running tasks. The button is dynamically
displayed based on task status (RUNNING/PENDING) and switches back to send button when
task is completed or cancelled.

Frontend changes:
- Add cancelTask API method in tasks.ts
- Add cancelTask method in TaskContext with isCancelling state
- Add stop button in ChatArea component with conditional rendering
- Add internationalization for stop button and cancel messages (en/zh-CN)

UI Features:
- Stop button only shows when task is RUNNING or PENDING
- Shows loading state while cancelling
- Displays success/error messages using antd message component
- Automatically refreshes task details after cancellation
- Uses StopIcon from heroicons

User Experience:
- Seamless transition between send and stop buttons
- Clear visual feedback during cancellation
- Maintains session continuity after stopping
- Users can continue conversation in same session
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (8)
executor_manager/executors/base.py (1)

32-47: Align stop interface with routing model (executor_namespace mismatch)

The new stop_task(self, executor_name: str) keeps the Executor interface simple, but the router’s StopExecutorRequest already accepts an executor_namespace that never gets propagated. That creates a small API mismatch and may constrain non-Docker implementations later.

Consider either:

  • Extending the interface now (e.g. stop_task(self, executor_name: str, executor_namespace: Optional[str] = None)) and threading it through, or
  • Dropping executor_namespace from the stop request until it is actually needed.
executor_manager/routers/routers.py (1)

140-173: Stop executor route works, but executor_namespace is unused and error handling can be tightened

The stop endpoint wiring is functionally correct for the Docker executor (fetch dispatcher, call stop_task, return its result). Two minor points:

  1. Unused executor_namespace

    • StopExecutorRequest accepts executor_namespace, but the handler ignores it and only passes request.executor_name into executor.stop_task(...).
    • If namespace is not needed for any current executor implementation, consider removing the field from the request model; if it is intended for Kubernetes or future backends, it should be threaded into the executor layer (and likely the Executor.stop_task interface) rather than silently dropped.
  2. Exception handling style (non-blocking)

    • The broad except Exception with logger.error(...) and HTTPException(500, ...) matches the existing delete route, but it’s less than ideal:
      • A narrower exception set or an else block for the happy path would satisfy Ruff’s TRY300 suggestion.
      • Using logger.exception(...) here would preserve the traceback as Ruff suggests (TRY400).
      • If you do keep the broad catch, consider raise HTTPException(...) from e to distinguish handler failures from downstream issues (TRY400/B904).

None of these are blockers, but addressing them would make the stop route more explicit and future-proof.

executor_manager/executors/docker/executor.py (1)

330-405: Docker stop_task behavior matches non-destructive “best-effort” design

The implementation correctly:

  • Verifies ownership before acting.
  • Uses get_container_ports and handles both failure and “no ports” cases.
  • Sends a POST to the container’s /stop endpoint with a bounded timeout and treats timeouts as acceptable, which aligns with the best‑effort semantics described in the PR.
  • Returns a structured {status, message|error_msg} result consistent with the rest of the executor layer.

Only optional polish items:

  • Consider logger.exception(...) in the outer except to capture tracebacks, and ideally narrow the except Exception if you want to satisfy Ruff’s BLE001/TRY400 suggestions.
  • If a consumer later needs to distinguish “partial” vs “failed” more formally, you might centralize the status strings as constants, but that’s not necessary for this PR.
backend/app/services/adapters/executor_kinds.py (2)

669-700: Consider adding exception chaining for better error traceability.

The exception handling catches RequestException but doesn't chain it when raising the new HTTPException. This loses the original traceback, making debugging harder.

Apply this diff to preserve the exception chain:

         except requests.exceptions.RequestException as e:
             raise HTTPException(
                 status_code=500,
                 detail=f"Error stopping executor task: {str(e)}"
-            )
+            ) from e

702-732: Consider adding exception chaining for better error traceability.

Similar to the sync version, preserve the original exception chain when raising HTTPException.

Apply this diff:

         except httpx.HTTPError as e:
             raise HTTPException(
                 status_code=500,
                 detail=f"Error stopping executor task: {str(e)}"
-            )
+            ) from e
frontend/src/features/tasks/contexts/taskContext.tsx (1)

226-248: Consider simplifying the refresh logic.

The function updates the tasks list manually (line 233), then conditionally refreshes the selected task detail (lines 236-238), and finally refreshes the entire tasks list (line 241). This creates redundancy:

  1. The manual setTasks update (line 233) will be immediately overwritten by refreshTasks() (line 241)
  2. If the selected task is being cancelled, refreshTasks() will fetch the updated task anyway

Consider simplifying to:

   const cancelTask = async (taskId: number) => {
     setIsCancelling(true);
     try {
-      const updatedTask = await taskApis.cancelTask(taskId);
-
-      // Update task in list
-      setTasks(prev => prev.map(t => (t.id === taskId ? updatedTask : t)));
+      await taskApis.cancelTask(taskId);

       // Update selected task detail if it's the same task
       if (selectedTaskDetail?.id === taskId) {
         await refreshSelectedTaskDetail(false);
       }

       // Refresh tasks to ensure consistency
       await refreshTasks();
     } catch (error) {
       console.error('Failed to cancel task:', error);
       throw error;
     } finally {
       setIsCancelling(false);
     }
   };
backend/app/services/adapters/task_kinds.py (2)

635-635: Minor style improvement: Simplify boolean comparison.

Per Python best practices, checking is_active == True can be simplified to just is_active.

Apply this diff:

         task = db.query(Kind).filter(
             Kind.id == task_id,
             Kind.user_id == user_id,
             Kind.kind == "Task",
-            Kind.is_active == True
+            Kind.is_active
         ).first()

712-718: Improve exception handling and logging.

The exception handler could be improved in two ways:

  1. Use logging.exception() instead of logging.error() to automatically include the traceback
  2. Chain the exception when raising HTTPException to preserve the error context

Apply this diff:

         except Exception as e:
             db.rollback()
-            logger.error(f"Error cancelling task {task_id}: {str(e)}")
+            logger.exception(f"Error cancelling task {task_id}: {str(e)}")
             raise HTTPException(
                 status_code=500,
                 detail=f"Error cancelling task: {str(e)}"
-            )
+            ) from e
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f93f4f0 and 6ae6f0f.

📒 Files selected for processing (12)
  • backend/app/api/endpoints/adapter/tasks.py (1 hunks)
  • backend/app/core/config.py (1 hunks)
  • backend/app/services/adapters/executor_kinds.py (2 hunks)
  • backend/app/services/adapters/task_kinds.py (3 hunks)
  • executor_manager/executors/base.py (1 hunks)
  • executor_manager/executors/docker/executor.py (1 hunks)
  • executor_manager/routers/routers.py (2 hunks)
  • frontend/src/apis/tasks.ts (1 hunks)
  • frontend/src/features/tasks/components/ChatArea.tsx (7 hunks)
  • frontend/src/features/tasks/contexts/taskContext.tsx (4 hunks)
  • frontend/src/i18n/locales/en/common.json (1 hunks)
  • frontend/src/i18n/locales/zh-CN/common.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
frontend/src/apis/tasks.ts (2)
frontend/src/types/api.ts (1)
  • Task (192-213)
frontend/src/apis/client.ts (1)
  • apiClient (105-105)
executor_manager/executors/base.py (1)
executor_manager/executors/docker/executor.py (1)
  • stop_task (330-405)
backend/app/api/endpoints/adapter/tasks.py (3)
backend/app/schemas/task.py (1)
  • TaskInDB (81-91)
backend/app/services/adapters/task_kinds.py (1)
  • cancel_task (621-718)
backend/app/core/security.py (1)
  • get_current_user (26-49)
executor_manager/executors/docker/executor.py (2)
executor_manager/executors/base.py (1)
  • stop_task (37-47)
executor_manager/executors/docker/utils.py (2)
  • check_container_ownership (147-178)
  • get_container_ports (343-408)
backend/app/services/adapters/task_kinds.py (3)
backend/app/schemas/subtask.py (1)
  • SubtaskStatus (21-27)
backend/app/api/endpoints/adapter/tasks.py (1)
  • cancel_task (117-123)
backend/app/services/adapters/executor_kinds.py (1)
  • stop_executor_task_sync (669-700)
executor_manager/routers/routers.py (2)
executor_manager/executors/base.py (1)
  • stop_task (37-47)
executor_manager/executors/docker/executor.py (1)
  • stop_task (330-405)
frontend/src/features/tasks/contexts/taskContext.tsx (1)
frontend/src/apis/tasks.ts (1)
  • taskApis (85-159)
🪛 Ruff (0.14.5)
backend/app/services/adapters/executor_kinds.py

697-700: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


699-699: Use explicit conversion flag

Replace with conversion flag

(RUF010)


729-732: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


731-731: Use explicit conversion flag

Replace with conversion flag

(RUF010)

backend/app/api/endpoints/adapter/tasks.py

119-119: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


120-120: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

executor_manager/executors/docker/executor.py

397-397: Use explicit conversion flag

Replace with conversion flag

(RUF010)


400-400: Do not catch blind exception: Exception

(BLE001)


401-401: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


404-404: Use explicit conversion flag

Replace with conversion flag

(RUF010)

backend/app/services/adapters/task_kinds.py

635-635: Avoid equality comparisons to True; use Kind.is_active: for truth checks

Replace with Kind.is_active

(E712)


665-668: Abstract raise to an inner function

(TRY301)


699-699: Do not catch blind exception: Exception

(BLE001)


701-701: Use explicit conversion flag

Replace with conversion flag

(RUF010)


712-712: Do not catch blind exception: Exception

(BLE001)


714-714: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


714-714: Use explicit conversion flag

Replace with conversion flag

(RUF010)


715-718: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


717-717: Use explicit conversion flag

Replace with conversion flag

(RUF010)

executor_manager/routers/routers.py

169-169: Consider moving this statement to an else block

(TRY300)


170-170: Do not catch blind exception: Exception

(BLE001)


171-171: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


172-172: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (8)
backend/app/core/config.py (1)

18-21: Stop-task URL wiring looks correct

EXECUTOR_STOP_TASK_URL matches the executor-manager stop endpoint and mirrors the existing delete URL pattern; configuration is clear and environment-overridable.

frontend/src/i18n/locales/zh-CN/common.json (1)

250-254: New task cancellation i18n keys look good (zh-CN)

task.stop, task.cancel_success, and task.cancel_failed are clear and consistent with the intended UX.

frontend/src/apis/tasks.ts (1)

145-147: cancelTask API is correctly wired to backend cancel endpoint

The cancelTask helper cleanly maps to POST /tasks/{id}/cancel and aligns with the existing taskApis style and expected Task return type.

frontend/src/i18n/locales/en/common.json (1)

250-254: English task cancellation i18n strings look good

The new task namespace (stop, cancel_success, cancel_failed) is clear and matches the intended cancellation UX, and is consistent with the zh-CN locale.

backend/app/api/endpoints/adapter/tasks.py (2)

106-115: Good improvement: explicit return message.

Adding an explicit success message for the delete endpoint improves API consistency and provides clearer feedback to clients.


116-123: Endpoint implementation looks correct.

The cancel endpoint follows the established patterns and properly delegates to the service layer. However, note that there's a critical bug in the service layer's cancel_task method (see comment on task_kinds.py) that needs to be fixed.

frontend/src/features/tasks/components/ChatArea.tsx (2)

211-223: Well-implemented task cancellation handler.

The function properly guards against missing task ID, handles success/error cases, displays appropriate user feedback, and refreshes the task detail. Error handling is robust with logging and user notification.


390-416: Conditional button rendering is well-implemented.

The logic correctly shows:

  • Stop button when a task is actively RUNNING or PENDING
  • Send button otherwise (new task or completed task)

The button states (loading/disabled) are properly managed, and the UI provides clear visual feedback to users.

Comment on lines +679 to +691
# Update subtasks status to CANCELLED
for subtask in task_subtasks:
if subtask.status in [SubtaskStatus.RUNNING, SubtaskStatus.PENDING]:
subtask.status = SubtaskStatus.CANCELLED
subtask.error_message = "User cancelled"
subtask.completed_at = datetime.now()
subtask.updated_at = datetime.now()

# Collect unique executor keys to notify (namespace + name)
unique_executor_keys = set()
for subtask in task_subtasks:
if subtask.executor_name and subtask.status == SubtaskStatus.RUNNING:
unique_executor_keys.add((subtask.executor_namespace, subtask.executor_name))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical logic error: Executor stop notifications will never be sent.

The code collects executor keys (lines 688-691) by checking subtask.status == SubtaskStatus.RUNNING, but this happens after all RUNNING subtasks have been updated to CANCELLED (lines 680-685). This means unique_executor_keys will always be empty, and no stop notifications will be sent to executors.

Apply this diff to fix the ordering:

+            # Collect unique executor keys to notify BEFORE updating status (namespace + name)
+            unique_executor_keys = set()
+            for subtask in task_subtasks:
+                if subtask.executor_name and subtask.status == SubtaskStatus.RUNNING:
+                    unique_executor_keys.add((subtask.executor_namespace, subtask.executor_name))
+
             # Update subtasks status to CANCELLED
             for subtask in task_subtasks:
                 if subtask.status in [SubtaskStatus.RUNNING, SubtaskStatus.PENDING]:
                     subtask.status = SubtaskStatus.CANCELLED
                     subtask.error_message = "User cancelled"
                     subtask.completed_at = datetime.now()
                     subtask.updated_at = datetime.now()

-            # Collect unique executor keys to notify (namespace + name)
-            unique_executor_keys = set()
-            for subtask in task_subtasks:
-                if subtask.executor_name and subtask.status == SubtaskStatus.RUNNING:
-                    unique_executor_keys.add((subtask.executor_namespace, subtask.executor_name))
-
             # Notify executors to stop task (best effort, don't delete containers)
🤖 Prompt for AI Agents
In backend/app/services/adapters/task_kinds.py around lines 679 to 691, the code
collects executor keys after mutating all RUNNING subtasks to CANCELLED so the
set is always empty; to fix, first iterate task_subtasks and collect unique
executor keys for subtasks that have executor_name and are currently RUNNING,
then in a separate loop update those same subtasks' status/error/completion
timestamps to CANCELLED; ensure you use the pre-mutation status check when
building unique_executor_keys so stop notifications will be sent.

@qdaxb qdaxb closed this Nov 27, 2025
@qdaxb qdaxb deleted the wegent/task-cancel-feature branch November 27, 2025 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants