summaryrefslogtreecommitdiff
path: root/internal/api/taskops.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/taskops.go')
-rw-r--r--internal/api/taskops.go14
1 files changed, 13 insertions, 1 deletions
diff --git a/internal/api/taskops.go b/internal/api/taskops.go
index 2b133ac..a2668f3 100644
--- a/internal/api/taskops.go
+++ b/internal/api/taskops.go
@@ -36,7 +36,9 @@ type badRequestError struct{ msg string }
func (e *badRequestError) Error() string { return e.msg }
-func badRequestf(format string, a ...any) error { return &badRequestError{msg: fmt.Sprintf(format, a...)} }
+func badRequestf(format string, a ...any) error {
+ return &badRequestError{msg: fmt.Sprintf(format, a...)}
+}
// submitTaskSpec describes a task a chatbot wants created and immediately run.
type submitTaskSpec struct {
@@ -127,6 +129,16 @@ func (s *Server) acceptTask(ctx context.Context, id string, actor event.Actor) e
if err != nil {
return errTaskNotFound
}
+ // BLOCKED -> COMPLETED is a valid task.ValidTransition (added so
+ // internal/executor.Pool's maybeUnblockParent can complete a subtask
+ // whose own children all finished), but that transition must only ever
+ // happen via that internal mechanism -- never via a direct accept call,
+ // which would bypass the subtask-completion invariant (or silently
+ // foreclose an open ask_user question) for a task that isn't actually
+ // done yet.
+ if t.State == task.StateBlocked {
+ return conflictf("task cannot be accepted while BLOCKED (awaiting subtasks or a question)")
+ }
if !task.ValidTransition(t.State, task.StateCompleted) {
return conflictf("task cannot be accepted from state %s", t.State)
}