vxcontrol/pentagi · error

failed to delete assistant %d: %w

Error message

failed to delete assistant %d: %w

What it means

DeleteAssistant wraps the error from fw.flowCtx.DB.DeleteAssistant(ctx, assistantID) — the database delete of the assistant row. The in-memory worker was already finished and removed; only the persistence layer failed, so the AssistantDeleted event is not published and the assistant record remains in the database.

Source

Thrown at backend/pkg/controller/flow.go:615

	return nil, fmt.Errorf("assistant %d not found", assistantID)
}

func (fw *flowWorker) DeleteAssistant(ctx context.Context, assistantID int64) error {
	fw.awsMX.Lock()
	defer fw.awsMX.Unlock()

	aw, ok := fw.aws[assistantID]
	if ok {
		if err := aw.Finish(ctx); err != nil {
			return fmt.Errorf("failed to finish assistant %d: %w", assistantID, err)
		}

		delete(fw.aws, assistantID)
	}

	if assistant, err := fw.flowCtx.DB.DeleteAssistant(ctx, assistantID); err != nil {
		return fmt.Errorf("failed to delete assistant %d: %w", assistantID, err)
	} else {
		fw.flowCtx.Publisher.AssistantDeleted(ctx, assistant)
	}

	return nil
}

func (fw *flowWorker) ListAssistants(ctx context.Context) []AssistantWorker {
	fw.awsMX.Lock()
	defer fw.awsMX.Unlock()

	assistants := make([]AssistantWorker, 0, len(fw.aws))
	for _, aw := range fw.aws {
		assistants = append(assistants, aw)
	}

	slices.SortFunc(assistants, func(a, b AssistantWorker) int {
		return int(a.GetAssistantID() - b.GetAssistantID())

View on GitHub (pinned to ea665308ba)

Solutions

  1. Inspect the wrapped DB error: if it's a connection/pool issue, verify PostgreSQL is reachable and pool settings are adequate.
  2. Handle the already-deleted race: check for sql.ErrNoRows / duplicate-key semantics and treat idempotent deletes as success.
  3. Retry the delete with a fresh context after the DB recovers; the assistant is already gone from memory.
  4. Ensure the DB migrations are current (goose) so the assistants table schema matches the queries.

Example fix

// before
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
err := fw.DeleteAssistant(ctx, id) // ctx expires mid-DELETE
// after
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := fw.DeleteAssistant(ctx, id)
Defensive patterns

Strategy: retry

Validate before calling

if err := db.PingContext(ctx); err != nil {
    return fmt.Errorf("database unreachable before delete: %w", err)
}

Type guard

func isNoRows(err error) bool {
    return errors.Is(err, sql.ErrNoRows)
}

Try / catch

if err := fw.DeleteAssistant(ctx, id); err != nil {
    if errors.Is(err, sql.ErrNoRows) {
        return nil // already deleted; idempotent success
    }
    if isTransientDBErr(err) {
        return retryWithBackoff(3, func() error { return fw.DeleteAssistant(ctx, id) })
    }
    return err
}

Prevention

When it happens

Trigger: Calling DeleteAssistant when the SQLC/database DeleteAssistant query fails: DB connection dropped, row already deleted (constraint or NoRows behavior), transaction deadlock, or context cancelled during the query.

Common situations: PostgreSQL restarted or connection pool exhausted under load; two concurrent DeleteAssistant calls racing on the same assistantID; migration drift leaving the assistants table in an unexpected state; long-running request whose ctx times out mid-query.

Related errors


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