gastownhall/beads · warning

failed to parse backup state: %w

Error message

failed to parse backup state: %w

What it means

loadBackupState wraps json.Unmarshal failures when .beads/backup_state.json exists but is not valid JSON for the backupState struct. The throttle state is corrupt, so the auto-backup machinery cannot determine the last backup time/commit.

Source

Thrown at cmd/bd/backup_export.go:68

	if err := os.MkdirAll(dir, 0700); err != nil {
		return "", fmt.Errorf("failed to create backup directory: %w", err)
	}
	return dir, nil
}

// loadBackupState reads the backup state file, returning a zero state if missing.
func loadBackupState(dir string) (*backupState, error) {
	path := filepath.Join(dir, "backup_state.json")
	data, err := os.ReadFile(path) //nolint:gosec // path is constructed internally
	if os.IsNotExist(err) {
		return &backupState{}, nil
	}
	if err != nil {
		return nil, fmt.Errorf("failed to read backup state: %w", err)
	}
	var state backupState
	if err := json.Unmarshal(data, &state); err != nil {
		return nil, fmt.Errorf("failed to parse backup state: %w", err)
	}
	return &state, nil
}

// saveBackupState writes the backup state file atomically.
func saveBackupState(dir string, state *backupState) error {
	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal backup state: %w", err)
	}
	return atomicWriteFile(filepath.Join(dir, "backup_state.json"), data)
}

// atomicWriteFile writes data to a same-directory temp file, fsyncs the
// temp file's own contents, then renames it into place. This avoids a
// truncated/partial file at path if the process crashes mid-write.
//
// Two caveats this does NOT cover, narrowing the "crash-safe" claim rather

View on GitHub (pinned to 71377f2769)

Solutions

  1. Delete the corrupt state file: rm .beads/backup_state.json — the next command recreates it and worst case performs an extra backup.
  2. Inspect the file (cat .beads/backup_state.json) to confirm corruption before deleting; it contains only throttle timestamps/commit hashes, no issue data.
  3. If corruption recurs, ensure bd isn't being killed mid-write (check for OOM kills, abrupt shutdowns) — modern versions write atomically via atomicWriteFile.
  4. No data-loss risk: this file only governs backup throttling; your issues live in Dolt.

Example fix

// shell
rm -f .beads/backup_state.json && bd sync  # state is regenerated with fresh throttle timestamps
Defensive patterns

Strategy: validation

Validate before calling

const p = '.beads/backup_state.json'
data, _ := os.ReadFile(p)
if len(data) > 0 {
    var probe map[string]any
    if err := json.Unmarshal(data, &probe); err != nil {
        os.Remove(p) // corrupt throttle state; safe to remove, no issue data
    }
}

Type guard

func isValidBackupState(b []byte) bool {
    var s backupState
    return json.Unmarshal(b, &s) == nil
}

Try / catch

state, err := loadBackupState(dir)
if err != nil {
    if strings.Contains(err.Error(), "parse backup state") {
        _ = os.Remove(filepath.Join(dir, "backup_state.json"))
        state = &backupState{} // regenerate; worst case an extra backup runs
    } else {
        return err
    }
}

Prevention

When it happens

Trigger: backup_state.json contains truncated content (interrupted write from an older non-atomic version), an empty file, invalid characters, or JSON whose types don't match backupState fields.

Common situations: Power loss or kill -9 during a legacy write that predated atomicWriteFile; manual editing of .beads files; file truncated to 0 bytes by a crash or sync tool; copying repos with partially synced .beads state.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/ecb2db16288ec416. Report an issue: GitHub.