t8y2/dbx · error

manual transaction already open

Error message

manual transaction already open

What it means

beginManualTransaction pins a single exclusive physical connection for manual (explicit BEGIN/COMMIT/ROLLBACK) transaction control. Only one manual transaction can be active per session because that connection is held; if s.manualTx is already non-nil, starting another would leak or corrupt the pinned connection, so it refuses.

Source

Thrown at agents/drivers/oracle-go/main.go:1183

	params.URLParams = values.Encode()
	return params
}

func (s *server) disconnect() error {
	s.closeAllQuerySessions()
	_ = s.rollbackManualTransactionQuiet()
	s.legacyLOBFetchDeferred = false
	if s.db == nil {
		return nil
	}
	err := s.db.Close()
	s.db = nil
	return err
}

func (s *server) beginManualTransaction(schema string) error {
	if s.manualTx != nil {
		return errors.New("manual transaction already open")
	}
	db, err := s.requireDB()
	if err != nil {
		return err
	}
	// Hold one exclusive physical connection so DML/SELECT/schema stay on the
	// same Oracle session for the life of the interactive transaction.
	conn, err := db.Conn(context.Background())
	if err != nil {
		return fmt.Errorf("reserve connection for manual transaction: %w", err)
	}
	if strings.TrimSpace(schema) != "" {
		if _, err := conn.ExecContext(context.Background(), "ALTER SESSION SET CURRENT_SCHEMA = "+quoteIdentifier(schema)); err != nil {
			_ = conn.Close()
			return err
		}
	}
	tx, err := conn.BeginTx(context.Background(), nil)

View on GitHub (pinned to c0390bff16)

Solutions

  1. Commit or roll back the current manual transaction before beginning a new one.
  2. Use separate sessions (distinct agentSessionIds) for concurrent transactions.
  3. On transaction failure, always issue rollback in the error path so manualTx is cleared.
  4. Alternatively use auto-commit/implicit transactions if nesting is not needed.

Example fix

// before
s.beginManualTransaction(schema)
if err := doWork(); err != nil { return err } // no rollback, tx stays open
// after
s.beginManualTransaction(schema)
if err := doWork(); err != nil {
    s.rollbackManualTransaction()
    return err
}
s.commitManualTransaction()
Defensive patterns

Strategy: try-catch

Validate before calling

if s.manualTx != nil {
    return errors.New("commit or rollback the open manual transaction first")
}

Try / catch

if err := s.beginManualTransaction(schema); err != nil {
    if err.Error() == "manual transaction already open" {
        if rbErr := s.rollbackManualTransaction(); rbErr != nil { return rbErr }
        return s.beginManualTransaction(schema)
    }
    return err
}

Prevention

When it happens

Trigger: Calling begin_transaction (manual mode) twice without an intervening commit or rollback on the same session.

Common situations: Error path that skipped commit/rollback before starting a new transaction, concurrent requests sharing one session, retry logic that re-begins after a failure without rolling back first.

Related errors


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/7b9ae42e2044bcd2. Report an issue: GitHub.