docs(openspec): add task-cancellation proposal, design, specs, tasks
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-15
|
||||
@@ -0,0 +1,266 @@
|
||||
## Context
|
||||
|
||||
Task cancellation has no implementation anywhere in the stack today:
|
||||
|
||||
- `core/models.py`'s `TaskStatus` reserves `"cancelled"` but no code path assigns it.
|
||||
`runtime/task.py::TaskRunner._interrupt_task()` marks `should_stop`-triggered
|
||||
interruption as `status="failed", failure_reason="execution interrupted"` — this is the
|
||||
only existing consumer of `should_stop`, and it conflates "lease was lost" with "user
|
||||
asked to stop" (both produce `failed`).
|
||||
- `cloud/scheduler.py`'s `ScheduledTaskStatus` is `queued | assigned | dispatched | done |
|
||||
failed` — no `cancelled`, no cancel operation on `TaskScheduler` or `CloudRepository`.
|
||||
- The internal Host↔Cloud protocol (`cloud/internal_api/models.py`) has exactly one
|
||||
channel from Cloud back to Host during execution: `LeaseRenewalResponse`, currently
|
||||
`{status: Literal["renewed"], lease_expires_at: datetime}`. There is no push channel;
|
||||
the Host Agent is purely outbound (heartbeat, claim long-poll, renew, report result).
|
||||
- `workflow/` already has the target shape: `WorkflowRunStatus` includes `cancelled`,
|
||||
`WorkflowRunner._drive()` checks `should_stop()` at each step boundary and calls
|
||||
`self._checkpoint(run, ..., "cancelled")`. This design generalizes that pattern to
|
||||
`TaskRunner`/`ScheduledTask` rather than inventing a new one.
|
||||
- Three prior changes (`cloud-console`, `cloud-control-plane-integration`,
|
||||
`cloud-console-governance`) explicitly deferred cancellation, citing "no
|
||||
scheduler/repository operation for this exists." This change adds that operation.
|
||||
|
||||
Existing collaborative-stop infrastructure this design reuses instead of replacing:
|
||||
`ActiveAssignmentRunner.run()` (`apps/device-host-agent/host_agent/lease.py`) races
|
||||
execution against a renewal loop; the renewal loop is the only place the Host Agent
|
||||
talks to Cloud while a task is running. `should_stop` already flows
|
||||
`ActiveAssignmentRunner` → `AssignmentExecutor.execute()` →
|
||||
`TaskRunner.run()`/`WorkflowRunner.run()`, checked at step boundaries (never mid-step).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Let an authorized caller cancel a task in `queued`, `assigned`, or `dispatched` state
|
||||
through the public SDK.
|
||||
- For `queued` tasks (never dispatched to a Host), cancellation is synchronous and
|
||||
immediate — no Host round-trip needed.
|
||||
- For `assigned`/`dispatched` tasks (a Host may be actively executing), cancellation is
|
||||
collaborative: the Host learns about it at its next lease renewal (≤ ~1/3 of
|
||||
`lease_duration_seconds`, matching the existing lease-loss detection latency) and stops
|
||||
at the next step boundary, exactly like a lost lease does today.
|
||||
- Make `cancelled` a real, reachable terminal state end-to-end: `core/models.py`'s
|
||||
`TaskStatus`, `cloud/scheduler.py`'s `ScheduledTaskStatus`, the Cloud Console, and the
|
||||
Host Agent local console.
|
||||
- Distinguish "cancelled by request" from "failed" in every layer that currently reports
|
||||
`should_stop`-triggered stops as generic failure, so operators can tell the two apart.
|
||||
- Cancellation is idempotent: cancelling an already-cancelled or already-terminal task is
|
||||
a no-op with a clear response, not an error that implies something changed.
|
||||
|
||||
**Non-Goals:**
|
||||
- No mid-step interruption. A single in-flight action (a tap, an LLM planning call) is
|
||||
never aborted mid-flight; the existing step-boundary granularity of `should_stop` is
|
||||
unchanged. A slow single step still finishes before a `dispatched` task's cancellation
|
||||
takes effect.
|
||||
- No forced/instant kill of a Host Agent process or its OS-level subprocess tree. This is
|
||||
cooperative cancellation only, consistent with the project's existing lease-loss
|
||||
handling — not a new capability class.
|
||||
- No per-task ownership/ACL model. The Cloud repository does not track which principal
|
||||
submitted a task; cancel authorization reuses the existing `tasks:submit` scope
|
||||
(whoever can submit a task can cancel any task). Adding submitter-scoped authorization
|
||||
is a distinct governance change, out of scope here (parallels
|
||||
`cloud-console-governance`'s existing target-based, not ownership-based, model).
|
||||
- No retry-from-UI or task editing. Only stopping a task early; resubmission remains a
|
||||
separate, already-existing "submit a new task" action.
|
||||
- No cancellation of individual workflow steps independent of the whole run; workflow
|
||||
cancellation continues to mean "stop the run," which `workflow/` already implements.
|
||||
- No change to `TaskDispatcher` (`cloud/dispatch.py`) — confirmed dead/dev-only code per
|
||||
prior investigation; not worth extending for cancellation.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: `LeaseRenewalResponse.cancel_requested: bool` is the only new wire signal
|
||||
|
||||
Rejected alternatives: (a) a new dedicated Host-polled "check cancellation" endpoint —
|
||||
adds a second polling loop and a second latency bound to reason about, when renewal
|
||||
already runs on a well-understood cadence; (b) a push/webhook mechanism — breaks the
|
||||
explicit "Host Agent operation requires no inbound cloud connection" requirement in
|
||||
`host-agent-protocol`'s spec, which is a hard architectural constraint, not just current
|
||||
practice.
|
||||
|
||||
`cancel_requested` defaults to `False`. When the Cloud repository's `renew_lease` finds
|
||||
the active `ScheduledTask` has a pending cancellation record, it still renews the lease
|
||||
normally (a task must keep a live lease while the Host works through shutdown) but flags
|
||||
`cancel_requested=True` in the response. The Host Agent's `_renew_while_running` loop
|
||||
treats this the same way it treats `StaleLeaseError`: mark the `LeaseGuard` (extended
|
||||
with a distinct reason string, e.g. `"cancellation requested by control plane"`) and let
|
||||
the existing `should_stop` composite (`guard.is_lost() or self._stop_requested.is_set()`)
|
||||
do the rest — no new boolean plumbed through `ActiveAssignmentRunner`/`AssignmentExecutor`
|
||||
signatures.
|
||||
|
||||
### D2: Cancellation request is durable state, not a fire-and-forget signal
|
||||
|
||||
A `POST .../cancel` on an `assigned`/`dispatched` task writes a `cancel_requested_at`
|
||||
timestamp (see D5 schema) rather than only flipping an in-memory flag or directly setting
|
||||
`status="cancelled"`. Rationale: the Host may not renew again for up to
|
||||
`lease_duration_seconds / 3`; if the Cloud process restarts in that window, an in-memory
|
||||
signal would be lost silently, defeating the cancellation with no user-visible error. A
|
||||
durable row survives restarts and is what `renew_lease` reads.
|
||||
|
||||
The scheduled task's `status` only transitions to `cancelled` once the Host has actually
|
||||
stopped and reported it (D3) — or, for the immediate `queued` case, synchronously in the
|
||||
same request. This means `assigned`/`dispatched` tasks pass through an intermediate,
|
||||
observable "cancellation pending" state (surfaced to callers as the existing `status`
|
||||
value plus a non-null `cancel_requested_at`, not a new status literal — see D6) rather
|
||||
than jumping straight to `cancelled` before the Host has confirmed.
|
||||
|
||||
### D3: Host reports `cancelled` as a new terminal outcome via the existing result-report endpoint
|
||||
|
||||
`TerminalResultRequest.status` (`internal_api/models.py`) is `Literal["done", "failed"]`.
|
||||
This design adds `"cancelled"` to that literal rather than inventing a parallel
|
||||
cancellation-report endpoint, because `report_result`'s idempotency/lease-validation
|
||||
logic (`record_task_result` in `sql_repository.py`) already handles exactly the
|
||||
concurrency shape needed (attempt/lease-id conflict checks, `already_recorded` replay
|
||||
safety) — duplicating it for a cancel-specific endpoint would be pure risk with no
|
||||
benefit. `AssignmentExecutionResult.status` (`host_agent/assignment.py`) similarly gains
|
||||
a `"cancelled"` value alongside `"done"`/`"failed"`, set when `TaskRunner.run()` /
|
||||
`WorkflowRunner.run()` returns because of a cancellation-flavored stop rather than a lost
|
||||
lease or genuine step failure (see D4).
|
||||
|
||||
### D4: `should_stop` becomes a richer signal than a bare boolean at the `TaskRunner` boundary
|
||||
|
||||
Today `StopRequested = Callable[[], bool]`. Distinguishing "cancelled" from "lease
|
||||
lost"/"shutdown requested" only matters for the *reason recorded on the terminal
|
||||
status* — the control-flow behavior (stop at the next step boundary) is identical. Rather
|
||||
than changing the `should_stop` callable's signature (which would ripple through
|
||||
`workflow/runner.py`, tests, and every caller), `TaskRunner._interrupt_task()` and
|
||||
`WorkflowRunner._drive()`'s stop branch gain an optional second callable,
|
||||
`stop_reason: Callable[[], str] | None`, defaulting to the existing generic
|
||||
`"execution interrupted"` string when absent. `ActiveAssignmentRunner` supplies a
|
||||
`stop_reason` that reads `LeaseGuard.reason` (already a string field) so `"cancellation
|
||||
requested by control plane"` vs `"lease rejected by control plane"` flows through
|
||||
unchanged plumbing. `TaskRunner._interrupt_task()` sets `status="cancelled"` when the
|
||||
reason string indicates cancellation, else keeps `status="failed"` (lease loss remains a
|
||||
failure, not a cancellation, matching current behavior for that case). This is the
|
||||
narrowest change that gets a real `cancelled` status without touching every
|
||||
`should_stop`-typed parameter across the codebase.
|
||||
|
||||
Alternative considered and rejected: add a third `CancelRequested` callable parallel to
|
||||
`should_stop`. Rejected because it doubles the number of callables threaded through
|
||||
`ActiveAssignmentRunner → AssignmentExecutor → TaskRunner/WorkflowRunner` for
|
||||
information that's only needed at the one moment execution actually stops.
|
||||
|
||||
### D5: Schema — one new nullable column pair on `scheduled_tasks`, no new table
|
||||
|
||||
`packages/cloud-platform/cloud/migrations/versions/0012_task_cancellation.py` adds to
|
||||
`ScheduledTaskRow`/`scheduled_tasks`:
|
||||
- `cancel_requested_at: str | None` (ISO datetime, nullable) — set when a cancel request
|
||||
is recorded against an `assigned`/`dispatched` task; cleared (`NULL`) when the task
|
||||
reaches any terminal status, so a subsequent, different attempt of the same task id (if
|
||||
retry-on-lease-expiry logic in `reap_expired_leases` requeues it) doesn't inherit a
|
||||
stale cancellation.
|
||||
|
||||
No new table: the volume and access pattern (one pending value per task, read on every
|
||||
renewal, written once per cancel call) don't justify the join/joinless-read trade-off a
|
||||
separate `task_cancellation_requests` table would add, and there is exactly one prior
|
||||
migration precedent for this shape — `0008_task_progress_columns` added nullable
|
||||
scalar columns directly to `scheduled_tasks` for the same reason (frequently-read,
|
||||
single-value-per-task state). `TaskAttemptRow`/`task_attempts` needs no schema change:
|
||||
its `status` column already accepts free-form strings and gains `"cancelled"` as a value
|
||||
alongside `"assigned"/"dispatched"/"expired"/"done"/"failed"`.
|
||||
|
||||
`CloudRepository` Protocol gains:
|
||||
- `request_task_cancellation(task_id, *, requested_at) -> CancellationRequestStatus`
|
||||
where `CancellationRequestStatus = Literal["requested", "already_terminal",
|
||||
"already_requested", "not_found"]` — synchronously transitions a `queued` task straight
|
||||
to `cancelled` (there's no Host attempt in flight to notify) and otherwise sets
|
||||
`cancel_requested_at` on an `assigned`/`dispatched` task.
|
||||
- `renew_lease` gains a `cancel_requested` boolean in its return path (or the caller
|
||||
re-reads the row — implementation detail left to tasks.md) so `internal_api/api.py`'s
|
||||
`renew_assignment` handler can populate `LeaseRenewalResponse.cancel_requested`.
|
||||
- `record_task_result` accepts `status: Literal["done", "failed", "cancelled"]`
|
||||
(widened from today's `TerminalTaskStatus = Literal["done", "failed"]`) and clears
|
||||
`cancel_requested_at` on write.
|
||||
|
||||
### D6: No new `ScheduledTaskStatus` value for "cancellation pending"
|
||||
|
||||
Considered adding `"cancelling"` as a distinct status between `assigned`/`dispatched` and
|
||||
`cancelled`. Rejected: it would ripple into every place that already pattern-matches the
|
||||
existing five-value `ScheduledTaskStatus` (scheduler assignment logic, device
|
||||
reservation/`list_reserved_device_ids`, Cloud Console status filter, SDK response
|
||||
`Literal`), each needing to decide whether "cancelling" behaves like "assigned" (device
|
||||
still reserved, task still excluded from re-assignment) — which it always would, making
|
||||
the new status a strict synonym with an extra bit of information. Instead,
|
||||
"cancellation pending" is expressed as `status="assigned"` (or `"dispatched"`) plus
|
||||
non-null `cancel_requested_at` — every existing status-based code path (assignment
|
||||
matching, device reservation, retry-on-expiry) keeps working unmodified, and callers
|
||||
that want to show "cancelling…" in a UI check the extra field.
|
||||
|
||||
### D7: Public cancel endpoint shape
|
||||
|
||||
`POST /v1/tasks/{task_id}/cancel`, `202 Accepted` for the pending (assigned/dispatched)
|
||||
case and `200 OK` for the immediate (queued) case, both returning a small
|
||||
`TaskCancellationResponse {task_id, status}` reflecting the resulting `ScheduledTask`
|
||||
status. Repeated calls against an already-cancelled or already-`cancel_requested_at`-set
|
||||
task return the same response idempotently (HTTP 200, not a conflict) — matching the
|
||||
project's established idempotency style for `report_result`/`renew_assignment`, which
|
||||
return `already_recorded`/success on replay rather than erroring. Calling cancel on a
|
||||
task already `done`/`failed` returns `409 Conflict` with a clear "task is already
|
||||
terminal" detail, mirroring `_stale_lease_conflict`'s existing shape.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Operators expect cancellation to be instant] → It isn't, by design (D1's latency
|
||||
bound). The Cloud Console surfaces the `cancel_requested_at` pending state distinctly
|
||||
(D6) so operators see "cancellation requested" rather than a UI that looks stuck; docs
|
||||
(`docs/CLOUD_DEPLOYMENT.md`) get an explicit latency note per the proposal's Impact
|
||||
section.
|
||||
- [A Host Agent that never renews again (already offline, or wedged past its lease) never
|
||||
learns about the cancellation] → Existing `reap_expired_leases` already requeues or
|
||||
fails such tasks once the lease expires; this change doesn't need new machinery for
|
||||
that case — an offline Host's task eventually reaches a terminal state via the existing
|
||||
reaper, at which point `cancel_requested_at` being non-null is irrelevant (task is
|
||||
already terminal). Worth noting in tasks.md that the reaper's requeue path should NOT
|
||||
requeue (re-assign) a task with `cancel_requested_at` set — it should mark it
|
||||
`cancelled` instead of `queued`, or the cancellation would be silently dropped on
|
||||
retry.
|
||||
- [`tasks:submit`-scoped cancel with no per-task ownership means any authorized submitter
|
||||
can cancel any other submitter's task] → Accepted for this change (Non-Goals); this
|
||||
matches the existing coarse-grained scope model everywhere else in the SDK (task
|
||||
reading is likewise not ownership-scoped) and narrowing it is `cloud-console-governance`
|
||||
territory, not this change's.
|
||||
- [Widening `TaskStatus`/`ScheduledTaskStatus`/`TaskAttemptRow.status` literals is a
|
||||
backward-compatible additive change in Python, but any exhaustive `switch`/discriminated
|
||||
union in the TypeScript Console (`types.ts`'s `TaskStatus`) will fail to compile until
|
||||
updated] → Caught at Console build time (Vite/`vue-tsc`), not runtime; tasks.md includes
|
||||
updating `cloud-console/src/types.ts` and any exhaustiveness-checked switch in the same
|
||||
commit as the backend change.
|
||||
- [Race: cancel request arrives between the Host's `claim_assignment` (queued→dispatched
|
||||
transition happens inside `claim_assignment`, not `assign_task`) and its first renewal]
|
||||
→ Already handled by D2's durable-row design: the cancel call checks current `status`
|
||||
regardless of exactly when the Host claims, and `renew_lease` always re-reads the
|
||||
current row, so there's no window where the signal is lost — only a window (bounded by
|
||||
D1's latency) where it hasn't been observed yet.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Ship the Alembic migration (0012) — additive nullable column, no backfill needed, safe
|
||||
to apply with the application running (existing tasks get `cancel_requested_at = NULL`,
|
||||
behaviorally identical to today).
|
||||
2. Deploy Cloud API with the new repository methods, internal `renew_assignment` response
|
||||
field (`cancel_requested`, defaults `False` — old Host Agents ignore unknown fields),
|
||||
and the new public cancel endpoint. This step alone is a no-op for existing Hosts:
|
||||
nothing calls the new endpoint yet, and `LeaseRenewalResponse` gaining an optional
|
||||
field is backward-compatible with any Host Agent version already deployed (Pydantic
|
||||
response models are additive-safe for JSON-decoding clients that only read known
|
||||
fields).
|
||||
3. Deploy Host Agents with the updated `lease.py`/`assignment.py`/`client.py` that read
|
||||
and act on `cancel_requested`. Hosts not yet updated simply never observe cancellation
|
||||
requests (task stays "pending cancellation" until its lease naturally expires and the
|
||||
reaper marks it `cancelled` per the Risks section) — a soft-fail, not a hard error.
|
||||
4. Ship the Cloud Console and Host Agent local console UI changes last, once the backend
|
||||
contract is stable.
|
||||
5. Rollback: the migration is purely additive and safe to leave in place even if the
|
||||
feature is disabled; no rollback migration is required beyond the standard Alembic
|
||||
downgrade (drop the column) if a full revert is ever needed.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether the public cancel endpoint should also accept an optional caller-supplied
|
||||
reason string (for audit/UI display) — left to tasks.md to decide during
|
||||
implementation; not a design-blocking question since it's purely additive to
|
||||
`TaskCancellationRequest` if added later.
|
||||
- Whether `reap_expired_leases`' "mark cancelled instead of requeue when
|
||||
`cancel_requested_at` is set" behavior (noted in Risks) needs its own explicit
|
||||
requirement in the `task-scheduler` delta spec or can be covered as an implementation
|
||||
detail of the existing restart-recovery requirement — resolve when writing specs.md.
|
||||
@@ -0,0 +1,81 @@
|
||||
## 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.
|
||||
@@ -0,0 +1,24 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Task dashboard
|
||||
The console SHALL render a task view listing tasks by status with pagination, and SHALL show a task's detail including its attempt history, using the platform SDK's task-listing and attempt-history endpoints. The task detail view SHALL offer a Cancel action for tasks in status `queued`, `assigned`, or `dispatched`, using the platform SDK's cancel endpoint, and the status filter SHALL include `cancelled`.
|
||||
|
||||
#### Scenario: Browse the task queue
|
||||
- **WHEN** an operator with a `tasks:read`-scoped token opens the task view
|
||||
- **THEN** the console displays tasks with their status, goal or workflow reference, and assigned device/host, and lets the operator filter by status, including `cancelled`
|
||||
|
||||
#### Scenario: Inspect a task's attempt history
|
||||
- **WHEN** an operator selects a task from the list
|
||||
- **THEN** the console displays that task's recorded attempts in order, including each attempt's outcome
|
||||
|
||||
#### Scenario: Cancel a task from the detail view
|
||||
- **WHEN** an operator with a `tasks:submit`-scoped token views the detail of a task whose status is `queued`, `assigned`, or `dispatched`, and clicks Cancel
|
||||
- **THEN** the console calls the cancel endpoint and updates the displayed status to reflect the immediate or pending cancellation result
|
||||
|
||||
#### Scenario: Cancel action is absent for terminal tasks
|
||||
- **WHEN** an operator views the detail of a task whose status is `done`, `failed`, or `cancelled`
|
||||
- **THEN** the console does not offer a Cancel action for that task
|
||||
|
||||
#### Scenario: Cancel attempted without submit scope
|
||||
- **WHEN** an operator whose token lacks `tasks:submit` views a cancellable task's detail
|
||||
- **THEN** the console does not offer a Cancel action, or surfaces the API's authorization error without implying the task was cancelled
|
||||
@@ -0,0 +1,48 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Active execution renews its lease
|
||||
The Host Agent SHALL renew the active assignment lease before expiry while execution continues, and SHALL treat loss or rejection of the lease, or an observed cancellation request, as a stop condition for further planned actions where interruption is possible.
|
||||
|
||||
#### Scenario: Lease renewal succeeds
|
||||
- **WHEN** the owning Host Agent renews an unexpired active lease
|
||||
- **THEN** the control plane extends its expiry without changing the task attempt or device assignment
|
||||
|
||||
#### Scenario: Lease is stale or foreign
|
||||
- **WHEN** a Host Agent attempts to renew an expired, replaced, or differently owned lease
|
||||
- **THEN** the control plane returns a conflict and does not revive or alter the current attempt
|
||||
|
||||
#### Scenario: Renewal response signals a pending cancellation
|
||||
- **WHEN** the control plane's renewal response for an active lease indicates a pending cancellation request
|
||||
- **THEN** the Host Agent stops further planned actions at the next available step boundary, the same way it stops on lease loss
|
||||
|
||||
### Requirement: Terminal result reporting is idempotent
|
||||
The Host Agent SHALL report a terminal result using the task, attempt, and lease identifiers, and repeating the same report SHALL return the already recorded outcome without duplicating state transitions. The terminal result SHALL be `done`, `failed`, or `cancelled`.
|
||||
|
||||
#### Scenario: Report a successful result
|
||||
- **WHEN** the active lease owner reports successful completion
|
||||
- **THEN** the control plane marks the scheduled task done, releases the device reservation, and records the result metadata
|
||||
|
||||
#### Scenario: Report a cancelled result
|
||||
- **WHEN** the active lease owner reports that its execution stopped because of an observed cancellation request
|
||||
- **THEN** the control plane marks the scheduled task cancelled, releases the device reservation, and records the result metadata
|
||||
|
||||
#### Scenario: Retry a result after response loss
|
||||
- **WHEN** the Host Agent repeats the identical terminal report for an already completed active lease
|
||||
- **THEN** the control plane returns the recorded terminal result without creating a new attempt or error
|
||||
|
||||
#### Scenario: Stale attempt reports after requeue
|
||||
- **WHEN** an expired earlier attempt reports after a newer attempt has been created
|
||||
- **THEN** the control plane rejects the stale report and preserves the newer attempt's state
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Host distinguishes cancellation stop from lease-loss stop when reporting outcome
|
||||
The Host Agent SHALL track whether its active execution stopped because of an observed cancellation request or for another stop reason (lost or rejected lease), and SHALL report `cancelled` only in the cancellation case, reporting `failed` for other stop reasons.
|
||||
|
||||
#### Scenario: Stop triggered by cancellation
|
||||
- **WHEN** the Host Agent's collaborative-stop mechanism is triggered by a renewal response signaling a pending cancellation
|
||||
- **THEN** the terminal result it reports for that attempt is `cancelled`
|
||||
|
||||
#### Scenario: Stop triggered by lease loss
|
||||
- **WHEN** the Host Agent's collaborative-stop mechanism is triggered by a rejected or lost lease unrelated to any cancellation signal
|
||||
- **THEN** the terminal result it reports for that attempt is `failed`, not `cancelled`
|
||||
@@ -0,0 +1,54 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Task submission and status via the SDK
|
||||
The system SHALL allow an external integrator to submit a task (goal or workflow reference plus constraints) through the platform SDK's API, to query that task's current status by id, and to request cancellation of that task by id, backed by the `task-scheduler` capability.
|
||||
|
||||
#### Scenario: Submit a task via the API
|
||||
- **WHEN** an integrator calls the task-submission endpoint with a valid goal and optional constraints
|
||||
- **THEN** the API returns a task id that can be used to poll status, and the underlying `task-scheduler` records a new `queued` `ScheduledTask`
|
||||
|
||||
#### Scenario: Query status of a known task
|
||||
- **WHEN** an integrator requests status for a task id that exists
|
||||
- **THEN** the API returns that task's current status (`queued`, `assigned`, `dispatched`, `done`, `failed`, or `cancelled`)
|
||||
|
||||
#### Scenario: Query status of an unknown task
|
||||
- **WHEN** an integrator requests status for a task id that does not exist
|
||||
- **THEN** the API returns a not-found response rather than an unhandled server error
|
||||
|
||||
#### Scenario: Cancel a known task via the API
|
||||
- **WHEN** an integrator with the required scope calls the cancel endpoint for a task id that exists and is not already `done` or `failed`
|
||||
- **THEN** the API accepts the request and the underlying `task-scheduler` records the cancellation per its immediate or collaborative rules for that task's current status
|
||||
|
||||
#### Scenario: Cancel an unknown task
|
||||
- **WHEN** an integrator calls the cancel endpoint for a task id that does not exist
|
||||
- **THEN** the API returns a not-found response rather than an unhandled server error
|
||||
|
||||
### Requirement: Public API operations enforce scopes
|
||||
The public platform API SHALL require operation-specific scopes, including task submission, task cancellation, task reading, pool reading, plugin reading, and plugin administration.
|
||||
|
||||
#### Scenario: Submit token has task scope
|
||||
- **WHEN** a principal with `tasks:submit` calls the task-submission endpoint
|
||||
- **THEN** the request is authorized subject to normal task validation
|
||||
|
||||
#### Scenario: Submit-scoped token cancels a task
|
||||
- **WHEN** a principal with `tasks:submit` calls the task-cancellation endpoint for any task id
|
||||
- **THEN** the request is authorized; the platform SDK does not restrict cancellation to the task's original submitter, since no per-task submitter identity is tracked
|
||||
|
||||
#### Scenario: Read-only token attempts cancellation
|
||||
- **WHEN** a principal that holds only `tasks:read` calls the task-cancellation endpoint
|
||||
- **THEN** the API rejects the request before contacting the scheduler
|
||||
|
||||
#### Scenario: Non-admin token attempts plugin registration
|
||||
- **WHEN** an authenticated principal without `plugins:admin` calls plugin registration
|
||||
- **THEN** the API rejects the request before resolving or loading the plugin target
|
||||
|
||||
### Requirement: Python SDK client mirrors the REST API
|
||||
The system SHALL provide a Python client (`CloudClient`) exposing methods corresponding to each `/v1/...` route (submit task, get task status, cancel task, list devices, list hosts, list plugins, register plugin), so integrators do not need to hand-construct HTTP requests.
|
||||
|
||||
#### Scenario: Client submits a task and retrieves status
|
||||
- **WHEN** a caller uses `CloudClient` to submit a task and then fetch its status by the returned id
|
||||
- **THEN** the client's methods produce the same result as calling the corresponding `/v1/...` endpoints directly over HTTP
|
||||
|
||||
#### Scenario: Client cancels a task
|
||||
- **WHEN** a caller uses `CloudClient` to cancel a task by id
|
||||
- **THEN** the client's method produces the same result as calling the cancel endpoint directly over HTTP
|
||||
@@ -0,0 +1,59 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Queued task cancellation is immediate
|
||||
The system SHALL, when a cancellation is requested against a task in status `queued`, transition that task directly to status `cancelled` synchronously within the same request, without contacting any Host.
|
||||
|
||||
#### Scenario: Cancel a task that has not been assigned
|
||||
- **WHEN** an authorized caller requests cancellation of a task whose status is `queued`
|
||||
- **THEN** the task's status becomes `cancelled` in the same request and no assignment or lease is ever created for it
|
||||
|
||||
### Requirement: In-flight task cancellation is a durable, collaborative request
|
||||
The system SHALL, when a cancellation is requested against a task in status `assigned` or `dispatched`, durably record a cancellation request against that task rather than immediately marking it `cancelled`, and SHALL surface that pending request to the owning Host Agent no later than its next lease renewal.
|
||||
|
||||
#### Scenario: Cancel a task currently executing on a Host
|
||||
- **WHEN** an authorized caller requests cancellation of a task whose status is `dispatched`
|
||||
- **THEN** the system records the cancellation request against the task's current attempt, the task's status remains `dispatched` until the Host reports a terminal result, and the request survives a control-plane restart
|
||||
|
||||
#### Scenario: Owning Host observes the pending cancellation at lease renewal
|
||||
- **WHEN** the Host Agent executing the task renews its lease after a cancellation request was recorded
|
||||
- **THEN** the renewal response signals the pending cancellation and the Host Agent stops further planned actions at the next available step boundary
|
||||
|
||||
#### Scenario: Cancellation is not instantaneous
|
||||
- **WHEN** a cancellation is requested against a `dispatched` task
|
||||
- **THEN** the system does not guarantee the task reaches status `cancelled` before the owning Host's next lease-renewal cycle completes
|
||||
|
||||
### Requirement: Host reports a cancelled outcome distinct from a failed outcome
|
||||
The Host Agent SHALL report a terminal status of `cancelled`, distinct from `failed`, when its active execution stopped because of an observed cancellation request rather than a lease loss or an execution error, and the control plane SHALL record that task as status `cancelled`.
|
||||
|
||||
#### Scenario: Execution stops due to a cancellation request
|
||||
- **WHEN** the Host Agent's active `TaskRunner` or `WorkflowRunner` execution stops because a lease renewal signaled a pending cancellation
|
||||
- **THEN** the Host Agent reports terminal status `cancelled`, and the control plane transitions the task to status `cancelled` and releases its device reservation
|
||||
|
||||
#### Scenario: Execution stops due to lease loss unrelated to cancellation
|
||||
- **WHEN** the Host Agent's active execution stops because its lease was rejected or lost for a reason other than a pending cancellation
|
||||
- **THEN** the Host Agent reports terminal status `failed`, not `cancelled`
|
||||
|
||||
### Requirement: Cancellation requests are idempotent
|
||||
The system SHALL treat a repeated cancellation request against a task that already has a pending or completed cancellation as a no-op that returns the task's current status, rather than as an error.
|
||||
|
||||
#### Scenario: Cancel a task twice
|
||||
- **WHEN** an authorized caller requests cancellation of a task that already has a pending cancellation request recorded
|
||||
- **THEN** the system returns the same successful response as the first request without creating a duplicate cancellation record
|
||||
|
||||
#### Scenario: Cancel an already-cancelled task
|
||||
- **WHEN** an authorized caller requests cancellation of a task whose status is already `cancelled`
|
||||
- **THEN** the system returns success reflecting the `cancelled` status without error
|
||||
|
||||
### Requirement: Cancellation is rejected for tasks already in a terminal, non-cancelled state
|
||||
The system SHALL reject a cancellation request against a task whose status is already `done` or `failed` with a clear conflict error, without altering that task's recorded outcome.
|
||||
|
||||
#### Scenario: Cancel a completed task
|
||||
- **WHEN** an authorized caller requests cancellation of a task whose status is `done`
|
||||
- **THEN** the system rejects the request with a conflict error and the task's status and result remain unchanged
|
||||
|
||||
### Requirement: An expiring lease on a task with a pending cancellation resolves to cancelled, not requeued
|
||||
The system SHALL, when an active lease expires on a task that has a pending cancellation request, mark that task `cancelled` rather than returning it to `queued` for a further attempt.
|
||||
|
||||
#### Scenario: Lease expires while a cancellation is pending
|
||||
- **WHEN** the active lease on a `dispatched` task with a pending cancellation request expires before a terminal result is reported
|
||||
- **THEN** the task transitions to status `cancelled` and its device reservation is released, instead of being requeued for another attempt
|
||||
@@ -0,0 +1,54 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Terminal transitions validate the active lease
|
||||
The system SHALL accept a `done`, `failed`, or `cancelled` result only from the current active task attempt and lease and SHALL make repeated identical terminal reports idempotent.
|
||||
|
||||
#### Scenario: Active lease reports completion
|
||||
- **WHEN** the active lease owner reports a terminal result
|
||||
- **THEN** the task transitions once to done, failed, or cancelled and releases its device reservation
|
||||
|
||||
#### Scenario: Superseded lease reports completion
|
||||
- **WHEN** a result references a lease superseded by expiry and retry
|
||||
- **THEN** the result is rejected and cannot overwrite the current task attempt
|
||||
|
||||
### Requirement: Expired attempts follow bounded retry policy
|
||||
The system SHALL detect expired assigned or dispatched leases and SHALL either requeue the task with its reservation released, mark it failed when the configured attempt limit is reached, or mark it cancelled when a cancellation request is pending against it.
|
||||
|
||||
#### Scenario: Lease expires with attempts remaining
|
||||
- **WHEN** an active lease expires before a terminal result and the task has remaining attempts and no pending cancellation request
|
||||
- **THEN** the task returns to queued, the previous device reservation is released, and the expired attempt remains auditable
|
||||
|
||||
#### Scenario: Lease expires at attempt limit
|
||||
- **WHEN** an active lease expires and the task has reached its maximum attempts
|
||||
- **THEN** the task becomes failed with a lease-expiry reason and its device reservation is released
|
||||
|
||||
#### Scenario: Lease expires with a cancellation pending
|
||||
- **WHEN** an active lease expires on a task that has a pending cancellation request, regardless of remaining attempts
|
||||
- **THEN** the task becomes cancelled rather than being requeued or marked failed, and its device reservation is released
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Task status includes a reachable cancelled value
|
||||
The `ScheduledTaskStatus` SHALL include `cancelled` as a terminal status reachable from `queued`, `assigned`, or `dispatched`, alongside the existing `done` and `failed` terminal statuses.
|
||||
|
||||
#### Scenario: Cancelled status is a valid terminal state
|
||||
- **WHEN** a task's cancellation completes, whether immediately from `queued` or after collaborative stop from `assigned`/`dispatched`
|
||||
- **THEN** the task's status is `cancelled`, and no further assignment, claim, or lease-renewal operation is accepted against it
|
||||
|
||||
### Requirement: Cancellation requests are recorded durably against in-flight tasks
|
||||
The scheduler repository SHALL persist a cancellation request against an `assigned` or `dispatched` task's current attempt such that the request is observable across a control-plane process restart, before the task reaches a terminal status.
|
||||
|
||||
#### Scenario: Cancellation request survives a restart
|
||||
- **WHEN** a cancellation request is recorded against a `dispatched` task and the control plane process restarts before the Host next renews its lease
|
||||
- **THEN** the pending cancellation request is still present and is surfaced to the Host on its next renewal after restart
|
||||
|
||||
### Requirement: Lease renewal surfaces a pending cancellation request
|
||||
The scheduler repository's lease-renewal operation SHALL report whether the renewing attempt has a pending cancellation request, without altering the normal lease-extension outcome.
|
||||
|
||||
#### Scenario: Renewal on a task with a pending cancellation
|
||||
- **WHEN** the owning host renews the lease for an attempt that has a pending cancellation request
|
||||
- **THEN** the lease is extended normally and the renewal result additionally indicates the pending cancellation
|
||||
|
||||
#### Scenario: Renewal on a task without a pending cancellation
|
||||
- **WHEN** the owning host renews the lease for an attempt with no pending cancellation request
|
||||
- **THEN** the lease is extended normally and the renewal result indicates no pending cancellation
|
||||
@@ -0,0 +1,67 @@
|
||||
## 1. Core and Runtime status plumbing
|
||||
|
||||
- [ ] 1.1 Confirm `core/models.py`'s `TaskStatus` already includes `"cancelled"` (it does); add `StepStatus` no change needed — verify no other literal needs widening.
|
||||
- [ ] 1.2 Add an optional `stop_reason: Callable[[], str] | None` parameter to `TaskRunner.run()` (`runtime/task.py`), defaulting to `None`.
|
||||
- [ ] 1.3 Update `TaskRunner._interrupt_task()` to accept the resolved reason string, set `status="cancelled"` when the reason indicates cancellation (e.g. contains `"cancel"`), else keep `status="failed"` as today, and record the reason as `failure_reason` in both cases.
|
||||
- [ ] 1.4 Update `WorkflowRunner`'s stop branch (`workflow/runner.py`) to accept and pass through the same optional `stop_reason`, reusing its existing `"cancelled"` checkpoint call — confirm no behavior change needed since it already lands on `"cancelled"` for any stop; only wire the reason through for consistency/logging.
|
||||
- [ ] 1.5 Add/extend unit tests in `tests/` for `TaskRunner` covering: cancellation-flavored stop → `status="cancelled"`; lease-loss-flavored stop → `status="failed"` (existing behavior preserved).
|
||||
|
||||
## 2. Cloud persistence: schema and repository
|
||||
|
||||
- [ ] 2.1 Add Alembic migration `0012_task_cancellation.py` under `packages/cloud-platform/cloud/migrations/versions/`: add nullable `cancel_requested_at` column to `scheduled_tasks`, matching the style of `0008_task_progress_columns`.
|
||||
- [ ] 2.2 Add `cancel_requested_at` to `ScheduledTaskRow` (`db_models.py`) and to the `ScheduledTask` dataclass (`cloud/scheduler.py`).
|
||||
- [ ] 2.3 Widen `ScheduledTaskStatus` (`cloud/scheduler.py`) to include `"cancelled"`.
|
||||
- [ ] 2.4 Widen `TerminalTaskStatus` and `record_task_result`'s accepted status literal (`repository.py` Protocol, `sql_repository.py`) to include `"cancelled"`; ensure the idempotency logic (`already_recorded`/`conflict`) treats a repeated `"cancelled"` report the same way it treats repeated `"done"`/`"failed"` reports today.
|
||||
- [ ] 2.5 Add `CancellationRequestStatus = Literal["requested", "already_terminal", "already_requested", "not_found"]` and a `request_task_cancellation(task_id, *, requested_at) -> CancellationRequestStatus` method to the `CloudRepository` Protocol.
|
||||
- [ ] 2.6 Implement `request_task_cancellation` in `SQLAlchemyCloudRepository`: for `queued` tasks, transition directly to `status="cancelled"`; for `assigned`/`dispatched` tasks, set `cancel_requested_at` if unset (return `"already_requested"` if already set); for `done`/`failed`/`cancelled` tasks, return `"already_terminal"`/success-idempotent as appropriate per design D7; for unknown task id, return `"not_found"`.
|
||||
- [ ] 2.7 Extend `renew_lease` (`sql_repository.py`) to read `cancel_requested_at` on the current row and report whether it is set, without changing its existing lease-extension/progress-update behavior.
|
||||
- [ ] 2.8 Update `reap_expired_leases` so a task whose `cancel_requested_at` is set resolves to `status="cancelled"` (clearing `cancel_requested_at`) instead of being requeued to `queued`, regardless of remaining attempts.
|
||||
- [ ] 2.9 Ensure `record_task_result` and the immediate `queued`-cancellation path both clear `cancel_requested_at` on reaching any terminal status.
|
||||
- [ ] 2.10 Add/extend repository-level tests (SQLite, matching existing test style) covering: immediate queued cancel; durable cancel request on assigned/dispatched surviving a simulated restart (re-fetch); renewal reporting the pending flag; expired lease with pending cancellation resolving to `cancelled`; idempotent repeat cancel calls; cancel rejected on `done`/`failed`.
|
||||
|
||||
## 3. Internal Host↔Cloud protocol
|
||||
|
||||
- [ ] 3.1 Add `cancel_requested: bool = False` field to `LeaseRenewalResponse` (`internal_api/models.py`).
|
||||
- [ ] 3.2 Widen `TerminalResultRequest.status` (`internal_api/models.py`) to `Literal["done", "failed", "cancelled"]`.
|
||||
- [ ] 3.3 Update `renew_assignment` route (`internal_api/api.py`) to populate `LeaseRenewalResponse.cancel_requested` from the repository's `renew_lease` result.
|
||||
- [ ] 3.4 Update `report_result` route (`internal_api/api.py`) to accept and forward the `"cancelled"` status to `record_task_result`.
|
||||
- [ ] 3.5 Add/extend internal API tests covering a renewal response surfacing `cancel_requested=True` and a `"cancelled"` terminal report being accepted and idempotent on repeat.
|
||||
|
||||
## 4. Host Agent collaborative stop
|
||||
|
||||
- [ ] 4.1 Extend `LeaseGuard` (`host_agent/lease.py`) with a `reason` attribute already implied by `mark_lost(reason)` — confirm it's readable, add a `is_cancellation` helper or convention (e.g. reason string prefix) to distinguish cancellation from other lost-lease reasons.
|
||||
- [ ] 4.2 Update `ActiveAssignmentRunner._renew_while_running()` (`host_agent/lease.py`): when `response.cancel_requested` is true, call `guard.mark_lost("cancellation requested by control plane")` instead of continuing the renewal loop.
|
||||
- [ ] 4.3 Update `client.py`'s `renew()` to ensure `LeaseRenewalResponse.cancel_requested` deserializes correctly (should be automatic via Pydantic model update, but add a test).
|
||||
- [ ] 4.4 Update `AssignmentExecutor._execute_goal()` and `_execute_workflow()` (`host_agent/assignment.py`) to map a cancellation-flavored stop to `AssignmentExecutionResult.status = "cancelled"` (new value alongside `"done"`/`"failed"`), reading the underlying `Task`/`WorkflowRun` status (`"cancelled"`) instead of collapsing it to `"failed"`.
|
||||
- [ ] 4.5 Update `AssignmentProcessor.process()` (`host_agent/processor.py`) so its `status = "done" if execution.status == "done" else "failed"` mapping becomes a three-way mapping that preserves `"cancelled"`, and `report_result` is called with `status="cancelled"` in that case.
|
||||
- [ ] 4.6 Add/extend Host Agent tests covering: a renewal response with `cancel_requested=True` stops the active `should_stop`-driven loop; the resulting `AssignmentExecutionResult.status` and reported terminal status are `"cancelled"`; a lease-loss stop unrelated to cancellation still reports `"failed"`.
|
||||
|
||||
## 5. Public SDK endpoint
|
||||
|
||||
- [ ] 5.1 Add `POST /v1/tasks/{task_id}/cancel` route to `cloud/sdk/api.py`, scope-gated by `TASKS_SUBMIT_SCOPE`, calling the new `TaskScheduler`/repository cancellation operation.
|
||||
- [ ] 5.2 Add `TaskCancellationResponse {task_id, status}` model to `cloud/sdk/models.py` (or wherever SDK response models live); return `200 OK` for immediate/already-terminal-cancelled idempotent cases, `202 Accepted` for a newly recorded pending cancellation, `404` for unknown task id, `409 Conflict` for a `done`/`failed` task.
|
||||
- [ ] 5.3 Widen the `status_filter` `Literal` on `list_tasks` (`cloud/sdk/api.py`) to include `"cancelled"`.
|
||||
- [ ] 5.4 Add a `cancel_task(task_id)` method to `CloudClient` (`cloud/sdk/client.py`) mirroring the new route.
|
||||
- [ ] 5.5 Add/extend SDK-level tests: submit-scoped caller cancels a queued task (200, immediate); cancels a dispatched task (202, pending); read-only-scoped caller rejected before reaching the scheduler; cancel on unknown id (404); cancel on terminal id (409); repeat cancel calls idempotent (200).
|
||||
|
||||
## 6. Frontend: Cloud Console
|
||||
|
||||
- [ ] 6.1 Add `"cancelled"` to `TaskStatus` in `cloud-console/src/types.ts` and to the `STATUSES` array in `TasksView.vue`.
|
||||
- [ ] 6.2 Add a `cancelTask(taskId)` method to `cloud-console/src/api.ts`.
|
||||
- [ ] 6.3 Add a "Cancel" button to `TasksView.vue`'s task detail panel, visible only when the selected task's status is `queued`/`assigned`/`dispatched` and the operator's token has `tasks:submit`; on click, call `cancelTask` and refresh the displayed task.
|
||||
- [ ] 6.4 Add/extend Cloud Console component tests (existing test style) covering: Cancel button visibility per status/scope; successful cancel updates displayed status; error response is surfaced without falsely showing cancelled.
|
||||
|
||||
## 7. Frontend: Host Agent local console
|
||||
|
||||
- [ ] 7.1 Add a `POST /tasks/{id}/cancel` route to the Host Agent local console (`host_agent/web/app.py`) that calls through to the same cancellation path used by the collaborative-stop mechanism for a locally-tracked task, consistent with existing local console read routes.
|
||||
- [ ] 7.2 Add a Cancel button to `templates/task_detail.html` for tasks not yet in a terminal state.
|
||||
- [ ] 7.3 Add/extend local console tests covering the new route and template rendering.
|
||||
|
||||
## 8. Docs
|
||||
|
||||
- [ ] 8.1 Add a short section to `docs/CLOUD_DEPLOYMENT.md` documenting that cancellation is collaborative (not instantaneous) for `assigned`/`dispatched` tasks, bounded by roughly one third of the configured lease duration, with immediate effect for `queued` tasks.
|
||||
|
||||
## 9. End-to-end verification
|
||||
|
||||
- [ ] 9.1 Run the full test suite (`uv run pytest` at repo root, plus `cloud-console` frontend tests) and confirm no regressions in existing task-scheduler, host-agent-protocol, platform-sdk, or workflow-orchestration tests.
|
||||
- [ ] 9.2 Manually or via an integration test, exercise the full path: submit a task, cancel a `queued` task (immediate), submit and dispatch another task, cancel it mid-execution, and confirm it reaches `cancelled` within one lease-renewal cycle with `cancelled` visible in both the public API and the Cloud Console.
|
||||
Reference in New Issue
Block a user