vxcontrol/pentagi · error

subtask not found

Error message

subtask not found

What it means

GetSubtask looks up an in-memory SubtaskWorker by ID in the controller's map. Unlike PopSubtask, it only finds workers already created in this process — it does not load from the database. This sentinel-style error means no live worker exists for that ID.

Source

Thrown at backend/pkg/controller/subtasks.go:191

	subtasks := make([]SubtaskWorker, 0)
	for _, subtask := range stc.subtasks {
		subtasks = append(subtasks, subtask)
	}

	sort.Slice(subtasks, func(i, j int) bool {
		return subtasks[i].GetSubtaskID() < subtasks[j].GetSubtaskID()
	})

	return subtasks
}

func (stc *subtaskController) GetSubtask(ctx context.Context, subtaskID int64) (SubtaskWorker, error) {
	stc.mx.Lock()
	defer stc.mx.Unlock()

	subtask, ok := stc.subtasks[subtaskID]
	if !ok {
		return nil, fmt.Errorf("subtask not found")
	}

	return subtask, nil
}

// InvalidateSubtasks drops workers deleted outside this controller,
// preventing stale cache lookups.
func (stc *subtaskController) InvalidateSubtasks(subtaskIDs []int64) {
	if len(subtaskIDs) == 0 {
		return
	}

	stc.mx.Lock()
	defer stc.mx.Unlock()

	for _, id := range subtaskIDs {
		delete(stc.subtasks, id)
	}

View on GitHub (pinned to ea665308ba)

Solutions

  1. Verify the subtask belongs to the current task and process instance
  2. Use PopSubtask first to create/cache the worker before GetSubtask
  3. Treat this as 404 at the API layer, not a 500
  4. If it appears after restart, re-derive worker state from the DB instead of memory

Example fix

// before
subtask, err := stc.GetSubtask(ctx, subtaskID)
if err != nil {
    return err // surfaces as 500
}
// after
subtask, err := stc.GetSubtask(ctx, subtaskID)
if err != nil {
    if err.Error() == "subtask not found" {
        return nil, ErrNotFound // map to 404
    }
    return nil, err
}
Defensive patterns

Strategy: type-guard

Validate before calling

func workerExists(stc *subtaskController, id int64) bool {
    stc.mx.Lock(); defer stc.mx.Unlock()
    _, ok := stc.subtasks[id]
    return ok
}

Type guard

func isSubtaskNotFound(err error) bool {
    return err != nil && strings.Contains(err.Error(), "subtask not found")
}

Try / catch

subtask, err := stc.GetSubtask(ctx, subtaskID)
if err != nil {
    if isSubtaskNotFound(err) { return ErrNotFound /* map to 404 */ }
    return err
}

Prevention

When it happens

Trigger: Calling GetSubtask with an ID never popped in this process, after backend restart (map is memory-only), after InvalidateSubtasks dropped the worker (subtask deleted), or with a wrong/foreign subtask ID.

Common situations: Client UI asking for a subtask from a previous run after backend restart; race where a subtask was deleted while a status request was in flight; passing a subtask ID from a different task.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/0b9351903163511a. Report an issue: GitHub.