temporalio/temporal · error

invalid version history item event ID

Error message

invalid version history item event ID: %v, version: %v

What it means

NewVersionHistoryItem constructs a VersionHistoryItem and panics if either eventID or version is negative. Event IDs and transition versions in history branching are monotonically non-negative counters; a negative value means an upstream computation (e.g. event ID arithmetic during duplication or branch fork) underflowed, so the constructor rejects it as an invariant violation.

Solutions

  1. Clamp computed event IDs/versions to >= 0 (or >= 1 for real events) before calling NewVersionHistoryItem.
  2. In duplicate-until-LCA logic, stop iteration when reaching the LCA item instead of computing offset items below its ID.
  3. Validate decoded mutable state items before copying; reject or repair negative IDs at load time.
  4. Add unit tests for first-item LCA duplication cases (TestDuplicateUntilLCAItem_Failure covers the panic path).

Example fix

// before
newItem := versionhistory.NewVersionHistoryItem(lcaItem.EventId-1, lcaItem.Version) // can go negative
// after
newEventID := lcaItem.EventId - 1
if newEventID < 0 {
    return ErrInvalidVersionHistoryItem
}
newItem := versionhistory.NewVersionHistoryItem(newEventID, lcaItem.Version)
Defensive patterns

Strategy: validation

Validate before calling

func safeNewItem(eventID, version int64) (*historyspb.VersionHistoryItem, error) {
    if eventID < 0 || version < 0 {
        return nil, fmt.Errorf("invalid version history item event ID: %v, version: %v", eventID, version)
    }
    return versionhistory.NewVersionHistoryItem(eventID, version), nil
}

Type guard

func isValidItemInput(eventID, version int64) bool { return eventID >= 0 && version >= 0 }

Try / catch

// clamp/validate arithmetic results before constructing items
newID := lcaItem.EventId - 1
if newID < 0 { return ErrInvalidVersionHistoryItem }
item := versionhistory.NewVersionHistoryItem(newID, lcaItem.Version)

Prevention

When it happens

Trigger: Calling versionhistory.NewVersionHistoryItem(eventID, version) with eventID < 0 or version < 0 — typically from CopyVersionHistoryItem on a corrupt source item, or from arithmetic like previousItem.EventId - 1 / version decrements that go below zero.

Common situations: Duplicating history items until an LCA where the LCA is the first item (EventId 1) causing EventId-1 = 0 or negative offsets; underflow when subtracting event ID deltas after history truncation; corrupted mutable state with negative IDs loaded from storage.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/f04cdf32fc928961. Report an issue: GitHub.

Appendix: source

Thrown at common/persistence/versionhistory/version_history_item.go:12

package versionhistory

import (
	"fmt"

	historyspb "go.temporal.io/server/api/history/v1"
)

// NewVersionHistoryItem create a new instance of VersionHistoryItem.
func NewVersionHistoryItem(eventID int64, version int64) *historyspb.VersionHistoryItem {
	if eventID < 0 || version < 0 {
		panic(fmt.Sprintf("invalid version history item event ID: %v, version: %v", eventID, version))
	}

	return &historyspb.VersionHistoryItem{EventId: eventID, Version: version}
}

// CopyVersionHistoryItem create a new instance of VersionHistoryItem.
func CopyVersionHistoryItem(item *historyspb.VersionHistoryItem) *historyspb.VersionHistoryItem {
	return NewVersionHistoryItem(item.EventId, item.Version)
}

// IsEqualVersionHistoryItem checks whether version history items are equal
func IsEqualVersionHistoryItem(item1 *historyspb.VersionHistoryItem, item2 *historyspb.VersionHistoryItem) bool {
	return item1.EventId == item2.EventId && item1.Version == item2.Version
}

// IsEqualVersionHistoryItems checks whether version history items are equal
func IsEqualVersionHistoryItems(items1 []*historyspb.VersionHistoryItem, items2 []*historyspb.VersionHistoryItem) bool {
	if len(items1) != len(items2) {

View on GitHub (pinned to bde624efd1)