JuliusBrussee/caveman · error

invalid run-state contract

Error message

invalid run-state contract

What it means

runstate.read loads the JSON state file for a given home/port and enforces its contract: schema must equal Schema, port must equal the file's own port, PID >= 1, instance token non-empty, and owner exactly "wrap" or "start". Any violation means the file is truncated, corrupt, forged, or from an incompatible version, and it is rejected outright.

Source

Thrown at proxy/internal/runstate/runstate.go:140

	}
	if err := tmp.Close(); err != nil {
		return err
	}
	return os.Rename(tmpName, Path(home, state.Port))
}

func read(home string, port int) (State, error) {
	raw, err := os.ReadFile(Path(home, port))
	if err != nil {
		return State{}, err
	}
	var state State
	if err := json.Unmarshal(raw, &state); err != nil {
		return State{}, err
	}
	if state.Schema != Schema || state.Port != port || state.PID < 1 ||
		state.InstanceToken == "" || (state.Owner != "wrap" && state.Owner != "start") {
		return State{}, errors.New("invalid run-state contract")
	}
	return state, nil
}

type validators struct {
	alive      func(int) bool
	executable func(int) (string, error)
	bound      func(string) bool
}

func validate(state State, checks validators) bool {
	if !checks.alive(state.PID) {
		return false
	}
	exe, err := checks.executable(state.PID)
	if err != nil || !strings.Contains(strings.ToLower(filepath.Base(exe)), "caveman-proxy") {
		return false
	}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Delete the invalid state file (it is derived data) and let the owner process rewrite it on next start
  2. Ensure only one binary version writes to a given home during upgrades
  3. If writing state yourself, emit all contract fields: schema, port, pid >= 1, instance token, owner of wrap|start

Example fix

// before
state, err := runstate.Read(home, port)
if err != nil { return err }

// after
state, err := runstate.Read(home, port)
if err != nil {
    if errors.Is(err, os.ErrNotExist) { /* not running */ return nil }
    // invalid contract: treat as not-running and clear the corrupt file
    _ = os.Remove(runstate.Path(home, port))
    return nil
}
Defensive patterns

Strategy: try-catch

Try / catch

state, err := runstate.Read(home, port)
if err != nil {
    if errors.Is(err, os.ErrNotExist) {
        // no instance running
    } else {
        // contract violation: treat as stale and remove the corrupt file
        _ = os.Remove(runstate.Path(home, port))
    }
}

Prevention

When it happens

Trigger: Reading a run-state file that was partially written (crash mid-write), hand-edited, copied from a different port, produced by a different schema version, or spoofed by another process dropping a file in the home directory.

Common situations: Process killed during state-file write leaving truncated JSON fields that parse but fail the contract; stale state from an older binary after upgrade; users editing the file to change the port or PID.

Related errors


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