feat(FN-4429): document user-cancel move-to-todo contract

Fusion-Task-Id: FN-4429
Fusion-Task-Lineage: b010437b-0e58-43f6-81b3-40dcebbf0873
This commit is contained in:
Fusion
2026-05-14 10:31:09 -07:00
committed by gsxdsm
parent ce37bc73bd
commit c003f4b591
4 changed files with 16 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
User-initiated drag/move of an in-progress task back to todo now hard-cancels the active executor session, aborts running task work before disposal, and parks the task with `userPaused: true` so scheduler dispatch does not immediately restart it.

View File

@@ -221,6 +221,8 @@ const { stdout, stderr } = await execAsync(command, {
`execSync` is only acceptable for short, deterministic git plumbing (`git rev-parse`, `git branch -d`, `git worktree remove`, etc.). When in doubt, use async. `execSync` is only acceptable for short, deterministic git plumbing (`git rev-parse`, `git branch -d`, `git worktree remove`, etc.). When in doubt, use async.
User-initiated `moveTask(in-progress → todo)` is a hard cancel contract: executor listeners must abort active sessions before dispose, stop step/workflow subprocesses, and leave the task parked in `todo` with `userPaused` semantics intact. Engine-initiated rebounds (pause, stuck recovery, workflow rerun, self-healing) must continue to use default `moveSource: "engine"` plus the appropriate `preserve*` flags (`preserveResumeState`, `preserveProgress`, `preserveWorktree`) and must not set `userPaused`.
## Git Conventions ## Git Conventions
- Commit messages: `feat(FN-XXX):`, `fix(FN-XXX):`, `test(FN-XXX):` - Commit messages: `feat(FN-XXX):`, `fix(FN-XXX):`, `test(FN-XXX):`

View File

@@ -1100,6 +1100,13 @@ Task steps use statuses: `pending`, `in-progress`, `done`, `skipped`.
- **Pre-merge** steps run in executor (`runWorkflowSteps()`) — bypassed in fast mode - **Pre-merge** steps run in executor (`runWorkflowSteps()`) — bypassed in fast mode
- **Post-merge** steps run in merger (`runPostMergeWorkflowSteps()`) - **Post-merge** steps run in merger (`runPostMergeWorkflowSteps()`)
### User cancel via move-to-todo
- `TaskStore.moveTask()` accepts `moveSource: "user" | "engine"` (default `"engine"`) and emits `task:moved` with `source` so listeners can distinguish manual moves from engine rebounds.
- Manual `in-progress → todo` moves (dashboard route `/tasks/:id/move` with `moveSource: "user"`) atomically set `task.userPaused = true`; engine/default rebounds do not.
- Any move to `in-progress` clears `task.userPaused` in the same store write so explicit redispatch resumes normally.
- `TaskExecutor` treats manual `in-progress → todo` as hard cancel: it marks the task as user-canceled, aborts active session types before dispose/termination, and suppresses preserve-resume auto-bounces while logging `Execution canceled by user — leaving task in todo`.
- Scheduler dispatch loop skips `todo` tasks with `userPaused === true` (queues with a user-paused reason) until a user explicitly moves the task back to `in-progress`.
--- ---
### Stalled review detection ### Stalled review detection

View File

@@ -181,6 +181,8 @@ fn task unarchive FN-001
**Paused-state normalization on reopen:** When a task is moved from `in-progress`, `in-review`, or `done` back to `todo` or `triage` (retry/requeue), Fusion clears `task.paused` and `task.pausedByAgentId` to prevent contradictory `todo + paused` or `in-progress + paused` states. A paused task in `todo` is excluded from scheduler dispatch. **Paused-state normalization on reopen:** When a task is moved from `in-progress`, `in-review`, or `done` back to `todo` or `triage` (retry/requeue), Fusion clears `task.paused` and `task.pausedByAgentId` to prevent contradictory `todo + paused` or `in-progress + paused` states. A paused task in `todo` is excluded from scheduler dispatch.
**Manual cancel park (`userPaused`):** A user-initiated move from `in-progress` to `todo` (`moveSource: "user"`) sets `task.userPaused = true` and hard-cancels active executor work. Scheduler treats `userPaused` tasks as intentionally parked and will not auto-dispatch them until the task is explicitly moved back to `in-progress` (which clears `userPaused`). Engine/internal requeues (`moveSource: "engine"`) do not set this flag.
**Paused-state normalization on explicit completion:** When an agent calls `fn_task_done` on a paused task, Fusion clears `task.paused` and `task.pausedByAgentId` regardless of the task's column (`in-progress` or `todo`). `task.paused` prevents new work from starting, but does not block an agent from completing in-flight work and transitioning the task to `done`. The scheduler respects `globalPause` independently. **Paused-state normalization on explicit completion:** When an agent calls `fn_task_done` on a paused task, Fusion clears `task.paused` and `task.pausedByAgentId` regardless of the task's column (`in-progress` or `todo`). `task.paused` prevents new work from starting, but does not block an agent from completing in-flight work and transitioning the task to `done`. The scheduler respects `globalPause` independently.
**Global pause vs task pause:** `settings.globalPause` gates new scheduler dispatches and is checked by the `fn_task_done` handoff logic. Task-level `task.paused` is a per-task gate that blocks execution start. They are independent — a task can be paused individually even when `globalPause` is `false`, and clearing `task.paused` does not affect `globalPause`. **Global pause vs task pause:** `settings.globalPause` gates new scheduler dispatches and is checked by the `fn_task_done` handoff logic. Task-level `task.paused` is a per-task gate that blocks execution start. They are independent — a task can be paused individually even when `globalPause` is `false`, and clearing `task.paused` does not affect `globalPause`.