JuliusBrussee/caveman · error

open arm

Error message

open arm %q: %w

What it means

SwitchExperimentArm wraps the error from the INSERT that opens the new arm row after the previous arm was closed. This library throws it when starting the replacement arm fails, which can leave the experiment momentarily with no open arm (the close already succeeded).

Solutions

  1. Read the wrapped cause with errors.Unwrap to identify the SQLite failure.
  2. Retry the switch after resolving lock contention (busy_timeout).
  3. Verify experiment_arms schema (experiment_id, arm, started_at, ended_at).
  4. If the experiment is left armless, call SwitchExperimentArm again or StopExperiment to finalize cleanly.

Example fix

// before
if _, err := s.db.Exec(`INSERT INTO experiment_arms ...`, id, arm, now); err != nil {
    return fmt.Errorf("open arm %q: %w", arm, err)
}
// after
if _, err := s.db.Exec(`INSERT INTO experiment_arms ...`, id, arm, now); err != nil {
    return fmt.Errorf("open arm %q: %w", arm, err)
}
// caller: retry on busy
if errors.Is(err, sqlite.ErrBusy) { return store.SwitchExperimentArm(label, arm) }
Defensive patterns

Strategy: retry

Try / catch

exp, err := store.SwitchExperimentArm(label, arm)
if err != nil {
    // verify experiment state via LoadExperiment before retrying
    return fmt.Errorf("switch arm: %w", err)
}

Prevention

When it happens

Trigger: Calling Store.SwitchExperimentArm(label, arm) when 'INSERT INTO experiment_arms (experiment_id, arm, started_at)' fails: constraint violation, locked database, disk full.

Common situations: SQLITE_BUSY under concurrent writers; corrupted schema; disk quota exceeded mid-run.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/5b18595e712d1b59. Report an issue: GitHub.

Appendix: source

Thrown at proxy/internal/store/learn_experiment.go:139

	if err != nil {
		return Experiment{}, err
	}
	if experiment.StoppedAt != "" {
		return Experiment{}, fmt.Errorf("experiment %q is stopped", experiment.Label)
	}
	for _, existing := range experiment.Arms {
		if existing.EndedAt == "" && existing.Arm == arm {
			return experiment, nil
		}
	}
	now := learnOutcomeClock().UTC().Format(time.RFC3339)
	if _, err := s.db.Exec(`UPDATE experiment_arms SET ended_at = ?
		WHERE experiment_id = ? AND ended_at IS NULL`, now, experiment.id); err != nil {
		return Experiment{}, fmt.Errorf("close open arm: %w", err)
	}
	if _, err := s.db.Exec(`INSERT INTO experiment_arms (experiment_id, arm, started_at)
		VALUES (?, ?, ?)`, experiment.id, arm, now); err != nil {
		return Experiment{}, fmt.Errorf("open arm %q: %w", arm, err)
	}
	return s.LoadExperiment(label)
}

func (s *Store) StopExperiment(label string) (Experiment, error) {
	experiment, err := s.LoadExperiment(label)
	if err != nil {
		return Experiment{}, err
	}
	now := learnOutcomeClock().UTC().Format(time.RFC3339)
	if _, err := s.db.Exec(`UPDATE experiment_arms SET ended_at = ?
		WHERE experiment_id = ? AND ended_at IS NULL`, now, experiment.id); err != nil {
		return Experiment{}, fmt.Errorf("close open arm: %w", err)
	}
	if _, err := s.db.Exec(`UPDATE experiments SET stopped_at = ? WHERE id = ?`, now, experiment.id); err != nil {
		return Experiment{}, fmt.Errorf("stop experiment: %w", err)
	}
	return s.LoadExperiment(label)

View on GitHub (pinned to 3ee70a1026)