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
- Generate a non-empty id before calling StartTrial, e.g. trialID := fmt.Sprintf("trial-%d", time.Now().UnixNano()) or a UUID.
- If the id comes from env/flags, validate it at the CLI boundary (fail with a clear message before the store call).
- 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
- Generate the trial id in one place and pass the same variable to Start/Finish/Analyze.
- Validate ids at the CLI/env boundary with a clear usage message.
- Never derive the id independently at multiple call sites.
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
- cachebench: nil corpus reader
- message exceeds byte limit
- tool message requires tool_call_id
- cachebench: invalid corpus
- cachebench: no providers
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/4a2108fc76b186b5.
Report an issue: GitHub.