vxcontrol/pentagi · error

failed to create tool call log: %w

Error message

failed to create tool call log: %w

What it means

PutLog persists a tool call record via db.CreateToolcall before executing tool logic. This error wraps any PostgreSQL failure during that insert (connection loss, FK violation on flow_id/task_id/subtask_id, context cancellation, constraint violation). It is a wrapper, so the root cause is always in the %w-wrapped error.

Source

Thrown at backend/pkg/controller/tclog.go:68

	name string,
	args json.RawMessage,
	taskID *int64,
	subtaskID *int64,
) (int64, error) {
	w.mx.Lock()
	defer w.mx.Unlock()

	tc, err := w.db.CreateToolcall(ctx, database.CreateToolcallParams{
		CallID:    callID,
		Status:    database.ToolcallStatusRunning,
		Name:      name,
		Args:      args,
		FlowID:    w.flowID,
		TaskID:    database.Int64ToNullInt64(taskID),
		SubtaskID: database.Int64ToNullInt64(subtaskID),
	})
	if err != nil {
		return 0, fmt.Errorf("failed to create tool call log: %w", err)
	}

	w.pub.ToolCallLogAdded(ctx, tc)

	return tc.ID, nil
}

func (w *flowToolCallLogWorker) UpdateLogSuccess(
	ctx context.Context,
	id int64,
	result string,
	durationSeconds float64,
) error {
	tc, err := w.db.UpdateToolcallFinishedResult(ctx, database.UpdateToolcallFinishedResultParams{
		Result:          result,
		DurationSeconds: durationSeconds,
		ID:              id,
	})

View on GitHub (pinned to ea665308ba)

Solutions

  1. Inspect the wrapped cause with errors.Unwrap / %v to distinguish FK violation vs connection error
  2. Verify taskID/subtaskID belong to the same flow and still exist before calling PutLog
  3. Check PostgreSQL logs for the exact constraint (e.g. toolcalls_subtask_id_fkey)
  4. Ensure pgvector/Postgres container is healthy (docker compose ps, connection pool limits)
  5. Retry on transient errors (pq: connection reset, driver.ErrBadConn) only; do not retry FK violations

Example fix

// before
id, err := worker.PutLog(ctx, callID, name, args, &staleTaskID, &staleSubtaskID)
// after
var taskID, subtaskID int64 = staleTaskID, staleSubtaskID
if !taskExists(ctx, db, taskID) || !subtaskInFlow(ctx, db, subtaskID, flowID) {
    taskID, subtaskID = 0, 0 // pass nil instead of dangling FKs
}
id, err := worker.PutLog(ctx, callID, name, args, maybePtr(taskID), maybePtr(subtaskID))
Defensive patterns

Strategy: validation

Validate before calling

func canLog(ctx context.Context, q database.Querier, flowID int64, taskID, subtaskID *int64) error {
    if ctx.Err() != nil { return ctx.Err() }
    if taskID != nil {
        if _, err := q.GetFlowTask(ctx, flowID, *taskID); err != nil { return fmt.Errorf("task %d not in flow %d: %w", *taskID, flowID, err) }
    }
    if subtaskID != nil {
        if _, err := q.GetSubtask(ctx, *subtaskID); err != nil { return fmt.Errorf("subtask %d missing: %w", *subtaskID, err) }
    }
    return nil
}

Type guard

func isForeignKeyErr(err error) bool {
    var pgErr *pgconn.PgError
    return errors.As(err, &pgErr) && pgErr.Code == "23503"
}

Try / catch

id, err := worker.PutLog(ctx, callID, name, args, taskID, subtaskID)
if err != nil {
    switch {
    case isForeignKeyErr(err):
        log.Warn().Err(err).Msg("dangling task/subtask reference; logging without FKs")
        id, err = worker.PutLog(ctx, callID, name, args, nil, nil)
    case errors.Is(err, context.Canceled):
        return err
    default:
        return fmt.Errorf("putlog: %w", err)
    }
}

Prevention

When it happens

Trigger: Calling PutLog with a subtaskID whose row was already deleted (FK violation), a taskID from another flow, an invalid JSON args blob rejected by a jsonb column, or while the DB connection is down/canceled ctx.

Common situations: Agents racing subtask cleanup so the subtask row disappears mid-run; transient DB restarts during long flows; passing task IDs from a stale/foreign flow after flow recreation.

Related errors


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