vxcontrol/pentagi · error

failed to finish flow %d: %w

Error message

failed to finish flow %d: %w

What it means

FinishFlow looks up the in-memory flow worker for flowID and calls its Finish(ctx) method to terminate the flow; when that worker-level finish fails, the error is wrapped as "failed to finish flow %d: %w" and returned. This wrapping adds the flow ID context so the caller can identify which flow could not be shut down. The controller deliberately does not delete the flow from its map until Finish succeeds, so a failed finish leaves the flow worker loaded and retryable.

Source

Thrown at backend/pkg/controller/flows.go:386

	if err != nil {
		return fmt.Errorf("failed to stop flow %d: %w", flowID, err)
	}

	return nil
}

func (fc *flowController) FinishFlow(ctx context.Context, flowID int64) error {
	fc.mx.Lock()
	defer fc.mx.Unlock()

	flow, ok := fc.flows[flowID]
	if !ok {
		return ErrFlowNotFound
	}

	err := flow.Finish(ctx)
	if err != nil {
		return fmt.Errorf("failed to finish flow %d: %w", flowID, err)
	}

	delete(fc.flows, flowID)

	return nil
}

func (fc *flowController) RenameFlow(ctx context.Context, flowID int64, title string) error {
	fc.mx.Lock()
	defer fc.mx.Unlock()

	flow, ok := fc.flows[flowID]
	if !ok {
		return ErrFlowNotFound
	}

	return flow.Rename(ctx, title)
}

View on GitHub (pinned to ea665308ba)

Solutions

  1. Inspect the wrapped cause (errors.Unwrap / %w chain or server logs) to see the underlying DB or context error.
  2. Verify PostgreSQL connectivity and that the database migrations are up to date.
  3. Retry FinishFlow: it is safe because the flow is only removed from the map after a successful Finish.
  4. Check that the caller's context is not cancelled or too short for the finish work (e.g. don't finish flows with an already-expired request context).

Example fix

// before: context dies with the HTTP request, finish may abort
err := flows.FinishFlow(r.Context(), flowID)

// after: use a detached context with a sane timeout for teardown
ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 30*time.Second)
defer cancel()
err := flows.FinishFlow(ctx, flowID)
Defensive patterns

Strategy: retry

Validate before calling

// ensure the flow is still loaded before finishing
if _, err := flows.GetFlow(ctx, flowID); err != nil {
    return err // ErrFlowNotFound — nothing to finish
}

Type guard

null

Try / catch

if err := flows.FinishFlow(ctx, flowID); err != nil {
    var ctxErr error
    if errors.As(err, &ctxErr) && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
        // retry with a detached, time-bounded context
        ctx2, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
        defer cancel()
        err = flows.FinishFlow(ctx2, flowID)
    }
    return err
}

Prevention

When it happens

Trigger: Calling FlowController.FinishFlow(ctx, flowID) when the underlying flowWorker.Finish(ctx) returns an error — e.g. its database writes (status transition, worker record update) fail due to a DB connection problem, constraint violation, or the context is cancelled/deadlines out mid-finish.

Common situations: Database down or restarted while a flow is being finished; request context cancelled because the HTTP/GraphQL client disconnected before the finish transaction committed; serialized DB access contention when many flows finish simultaneously; calling FinishFlow after the DB row was already modified externally.

Related errors


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