## Why Task cancellation has been an explicit, repeatedly-acknowledged gap: `cloud-console`, `cloud-control-plane-integration`, and `cloud-console-governance` each excluded it from scope and deferred it to "a later lifecycle capability." Operators currently have no way to stop a queued, assigned, or in-flight task short of letting it run to completion, failure, or lease expiry — including tasks stuck against an offline device or a runaway plan. `core/models.py`'s `TaskStatus` already reserves a `"cancelled"` value that no code path ever sets, and `workflow/` already proves the collaborative-stop pattern this change extends to goal-based cloud tasks. ## What Changes - Add a `cancelled` scheduler-side task status (`cloud/scheduler.py`'s `ScheduledTaskStatus`) reachable from `queued`, `assigned`, and `dispatched`. - Add a public SDK endpoint (`POST /v1/tasks/{task_id}/cancel`, gated by the existing `tasks:submit` scope — the repository has no per-task submitter/ownership tracking to authorize against more narrowly) that immediately cancels a `queued` task and otherwise records a cancellation request against an `assigned`/`dispatched` task. - Add an internal Host Agent protocol signal: `LeaseRenewalResponse` gains a `cancel_requested: bool` field; the Cloud repository's `renew_lease` reports it when the active attempt has a pending cancellation. This is the only new edge on the existing outbound-only Host Agent protocol — no inbound push, no new endpoint on the Host side. - Extend the Host Agent's existing `should_stop` collaborative-stop mechanism (`ActiveAssignmentRunner` → `AssignmentExecutor` → `TaskRunner`/`WorkflowRunner`) so a `cancel_requested` signal observed at lease-renewal time stops execution the same way a lost lease does today, and reports a `cancelled`-flavored terminal result. - Add `"cancelled"` as a real, reachable value of `core/models.py`'s `TaskStatus` (the `_interrupt_task` path already flowing through `should_stop` gains a cancellation-vs-lease-loss distinction) and confirm `TerminalResultRequest.status` can express it end to end. - Add an Alembic migration recording cancellation request/acknowledgement metadata on `scheduled_tasks`/`task_attempts` (requestor, requested-at, and the terminal `cancelled` outcome) — no schema change to unrelated tables. - Add a "Cancel" action to the Cloud Console `TasksView.vue` task-detail panel (visible for `queued`/`assigned`/`dispatched` tasks the operator is authorized to act on) and to the status filter dropdown; add a matching `POST /tasks/{id}/cancel` route + button to the Host Agent local console's task detail page for Host-local visibility/action on tasks running on that Host. - **BREAKING**: none of the existing status literals are renamed or removed; `cancelled` is purely additive. Callers that exhaustively `match`/switch over `TaskStatus` (Python) or `TaskStatus` (TypeScript) without a default arm will need to add a case — flagged in design.md's migration plan. ## Capabilities ### New Capabilities - `task-cancellation`: cancellation request lifecycle across `task-scheduler` (queued/ assigned/dispatched states), `host-agent-protocol` (collaborative cancel signal over lease renewal), and `agent-runtime`/workflow execution (stopping mid-task on a cancellation signal, distinct from lease loss). ### Modified Capabilities - `task-scheduler`: `ScheduledTaskStatus` gains `cancelled`; task submission/assignment requirements are unchanged, but the status-transition requirements need a new terminal transition path from `queued`/`assigned`/`dispatched`. - `host-agent-protocol`: the lease-renewal requirement ("Active execution renews its lease") gains a new SHALL for surfacing a cancellation request in the renewal response and treating it as a stop condition alongside lease loss. - `platform-sdk`: new cancel endpoint and scope-authorization requirement; task-status responses gain the `cancelled` status value. - `cloud-console-ui`: task list/detail view gains a Cancel action and the `cancelled` status value in filtering/display. ## Impact - **Cloud API / persistence**: `packages/cloud-platform/cloud/scheduler.py`, `repository.py` (Protocol), `sql_repository.py`, `db_models.py`, new Alembic migration, `internal_api/models.py` + `internal_api/api.py` (renew/claim/cancel routes), `sdk/api.py` + `sdk/models.py` (new public cancel endpoint), `auth.py` (scope reuse). - **Host Agent**: `apps/device-host-agent/host_agent/lease.py` (`ActiveAssignmentRunner` cancellation-aware stop), `client.py` (surface `cancel_requested` from renew response), `processor.py`/`assignment.py` (terminal status reporting), local console (`host_agent/web/app.py` + `templates/task_detail.html`) new cancel route. - **Runtime**: `core/models.py` (`TaskStatus` reachability), `runtime/task.py` (`_interrupt_task` cancellation-vs-interruption distinction), `workflow/runner.py` (reuse of the already-existing `cancelled` terminal status — no change needed there). - **Cloud Console frontend**: `cloud-console/src/views/TasksView.vue`, `src/types.ts`, `src/api.ts` (new `cancelTask` client method). - **Docs**: `docs/CLOUD_DEPLOYMENT.md` gets a short note on cancellation being collaborative (not instantaneous) and its ~1/3-lease-period latency bound.