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
- Inspect the wrapped cause (errors.Unwrap / %w chain or server logs) to see the underlying DB or context error.
- Verify PostgreSQL connectivity and that the database migrations are up to date.
- Retry FinishFlow: it is safe because the flow is only removed from the map after a successful Finish.
- 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
- Always finish flows with a context that outlives the HTTP request (context.WithoutCancel + timeout).
- Monitor PostgreSQL health; finish operations fail with the DB.
- Treat this error as retryable — the flow is only removed from the map after success.
- Log the unwrapped cause to distinguish DB errors from cancellation.
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
- failed to renew flow %d status: %w
- failed to get flow %d status: %w
- failed to stop flow %d: %w
- token not found in database
- failed to create flow in DB: %w
AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01).
Data as JSON: /api/errors/6d40e2e213745e4c.
Report an issue: GitHub.