JuliusBrussee/caveman · error

trial_id is required

Error message

trial_id is required

What it means

Store.StartTrial refuses to open a trial record when the trial identifier is the empty string. The check exists because trial_id is the primary key of the trial_runs table and every later operation (FinishTrial, AnalyzeTrial, payload joins) keys off it. An empty id would create an unusable or colliding row, so the store fails fast with a plain validation error before touching SQLite.

Source

Thrown at proxy/internal/store/trial_store.go:61

func (s *Store) RecordPayload(label, requestID, traceID string, body []byte) {
	trialID := strings.TrimPrefix(label, "trial:")
	if trialID == "" || trialID == label {
		return
	}
	_, err := s.db.Exec(
		`INSERT OR IGNORE INTO trial_payloads
		  (trial_id, request_id, trace_id, ts, request_bytes, raw_request)
		  VALUES (?, ?, ?, ?, ?, ?)`,
		trialID, requestID, traceID, time.Now().UTC().Format(time.RFC3339), len(body), append([]byte(nil), body...),
	)
	if err != nil && s.logger != nil {
		s.logger.Warn("local trial payload insert failed", "error", err, "request_id", requestID)
	}
}

func (s *Store) StartTrial(trialID, agentSlug, command string) error {
	if trialID == "" {
		return fmt.Errorf("trial_id is required")
	}
	_, err := s.db.Exec(
		`INSERT INTO trial_runs (trial_id, agent_slug, command, started_at)
		  VALUES (?, ?, ?, ?)
		  ON CONFLICT(trial_id) DO UPDATE SET
		    agent_slug = excluded.agent_slug,
		    command = excluded.command`,
		trialID, agentSlug, command, time.Now().UTC().Format(time.RFC3339),
	)
	return err
}

func (s *Store) FinishTrial(trialID string, exitCode int) error {
	if trialID == "" {
		return fmt.Errorf("trial_id is required")
	}
	_, err := s.db.Exec(
		`UPDATE trial_runs SET ended_at = ?, exit_code = ? WHERE trial_id = ?`,

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Generate a non-empty id before calling StartTrial, e.g. trialID := fmt.Sprintf("trial-%d", time.Now().UnixNano()) or a UUID.
  2. If the id comes from env/flags, validate it at the CLI boundary (fail with a clear message before the store call).
  3. Check the error with errors.Is-style string comparison or just treat any StartTrial error as fatal for the trial and report it to the operator.

Example fix

// before
s.StartTrial(os.Getenv("CAVE_TRIAL_ID"), slug, cmd)

// after
trialID := os.Getenv("CAVE_TRIAL_ID")
if trialID == "" {
    trialID = fmt.Sprintf("trial-%d", time.Now().UnixNano())
}
if err := s.StartTrial(trialID, slug, cmd); err != nil {
    log.Fatalf("start trial: %v", err)
}
Defensive patterns

Strategy: validation

Validate before calling

func validTrialID(id string) bool { return strings.TrimSpace(id) != "" }

if !validTrialID(trialID) {
    return errors.New("trial id required: set CAVE_TRIAL_ID or pass -trial-id")
}
err := s.StartTrial(trialID, slug, cmd)

Try / catch

if err := s.StartTrial(trialID, slug, cmd); err != nil {
    if strings.Contains(err.Error(), "trial_id is required") {
        // caller bug: fix id generation, do not retry
        return fmt.Errorf("cannot start trial: %w", err)
    }
    return err // database error
}

Prevention

When it happens

Trigger: Calling s.StartTrial("", "claude-code", "npm test") — e.g. the caller generated the id from an env var or CLI flag that was unset, or passed a struct field that was never populated before the call.

Common situations: A trial-orchestration script reads CAVE_TRIAL_ID (or similar) from the environment and it is empty in CI; a wrapper derives the id conditionally and the branch fell through; refactoring renamed the field carrying the id and the zero value reaches the store.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/4a2108fc76b186b5. Report an issue: GitHub.