vxcontrol/pentagi · error

failed to create termlog: %w

Error message

failed to create termlog: %w

What it means

PutMsg inserts the terminal log row via db.CreateTermLog after the container check passes. This error wraps the underlying database failure: FK violation (e.g. task/subtask rows deleted concurrently), connection loss, context cancellation, or oversized/invalid text data (text is UTF-8-sanitized first, so encoding is usually not the cause).

Source

Thrown at backend/pkg/controller/termlog.go:76

		tlw.containers = make(map[int64]struct{})
		for _, container := range containers {
			tlw.containers[container.ID] = struct{}{}
		}
		if _, ok := tlw.containers[containerID]; !ok {
			return 0, fmt.Errorf("container not found")
		}
	}

	termLog, err := tlw.db.CreateTermLog(ctx, database.CreateTermLogParams{
		Type:        msgType,
		Text:        database.SanitizeUTF8(msg),
		ContainerID: containerID,
		FlowID:      tlw.flowID,
		TaskID:      database.Int64ToNullInt64(taskID),
		SubtaskID:   database.Int64ToNullInt64(subtaskID),
	})
	if err != nil {
		return 0, fmt.Errorf("failed to create termlog: %w", err)
	}

	tlw.pub.TerminalLogAdded(ctx, termLog)

	return termLog.ID, nil
}

func (tlw *flowTermLogWorker) GetMsg(ctx context.Context, msgID int64) (database.Termlog, error) {
	msg, err := tlw.db.GetTermLog(ctx, msgID)
	if err != nil {
		return database.Termlog{}, fmt.Errorf("failed to get termlog: %w", err)
	}

	return msg, nil
}

func (tlw *flowTermLogWorker) GetContainers(ctx context.Context) ([]database.Container, error) {
	containers, err := tlw.db.GetFlowContainers(ctx, tlw.flowID)

View on GitHub (pinned to ea665308ba)

Solutions

  1. Inspect the wrapped cause to distinguish FK vs connection vs cancellation
  2. Validate taskID/subtaskID still exist and belong to the flow before logging
  3. Add bounded retry with backoff for transient driver errors on terminal writes
  4. Keep ctx alive for log writes (background context) so request cancellation doesn't drop terminal history
  5. Check DB logs for the failing constraint and fix the caller's ID lifecycle

Example fix

// before
if _, err := termWorker.PutMsg(ctx, t, line, cid, &taskID, &subtaskID); err != nil { return err }
// after
if _, err := termWorker.PutMsg(context.WithoutCancel(ctx), t, line, cid, &taskID, &subtaskID); err != nil {
    if isTransient(err) { return retryPutMsg(t, line, cid, taskID, subtaskID) }
    return err
}
Defensive patterns

Strategy: retry

Validate before calling

func canWriteTermLog(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 err }
    }
    if subtaskID != nil {
        if _, err := q.GetSubtask(ctx, *subtaskID); err != nil { return err }
    }
    return nil
}

Type guard

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

Try / catch

err := retry(3, backoff, func() error {
    _, err := w.PutMsg(ctx, msgType, line, containerID, taskID, subtaskID)
    if isForeignKeyErr(err) || errors.Is(err, context.Canceled) { return stop(err) }
    return err // transient errors are retried
})

Prevention

When it happens

Trigger: DB connection dropped during heavy terminal output; taskID/subtaskID pointing at rows deleted mid-execution; canceled context from a request timeout; violating a NOT NULL/length constraint on text.

Common situations: Long-running exec sessions surviving a Postgres restart; agents racing subtask completion/cleanup while still emitting output; very large paste/burst writes hitting timeouts.

Related errors


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