JuliusBrussee/caveman · warning

cacheengine: no stable prefix

Error message

cacheengine: no stable prefix

What it means

Returned by (*Engine).StartEpoch when stablePrefix(request.Segments, e.maxStablePrefixBytes) yields zero stable segments. An epoch must begin from a non-empty stable prefix — content that is marked stable and within the MaxStablePrefixBytes budget — because the entire epoch's cache boundaries are anchored to that prefix.

Source

Thrown at cacheengine/engine.go:239

}

// StartEpoch explicitly replaces frozen prefix state for one scope/profile epoch.
func (e *Engine) StartEpoch(request PlanRequest) (Plan, error) {
	if e == nil || e.guard == nil {
		return Plan{}, errors.New("cacheengine: nil engine")
	}
	if e.configErr != nil {
		return Plan{}, e.configErr
	}
	if err := validatePlanRequest(request); err != nil {
		return Plan{}, err
	}
	prefix, stableSegments, err := stablePrefix(request.Segments, e.maxStablePrefixBytes)
	if err != nil {
		return Plan{}, err
	}
	if len(stableSegments) == 0 {
		return Plan{}, errors.New("cacheengine: no stable prefix")
	}
	if len(cacheguard.DetectVolatile(prefix)) > 0 {
		return Plan{}, errors.New("cacheengine: volatile content cannot start stable epoch")
	}
	profile := normalizedProfile(request.Profile)
	economicsBasis := "modeled_input_rate_units"
	var warnings []string
	if !profile.EconomicsKnown {
		economicsBasis = "unavailable"
		warnings = []string{"cache_economics_unavailable"}
	}
	result, err := e.guard.StartNewEpoch(epochKey(request.Scope, request.Epoch, profile.ID), prefix)
	if err != nil {
		return Plan{}, err
	}
	return Plan{
		Decision:       DecisionNewEpoch,
		Reason:         string(cacheguard.DecisionNewEpoch),

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Ensure the head of the prompt contains at least one stable segment (static system instructions) before volatile content
  2. Verify segment stability flags are set correctly for your pipeline
  3. If the stable content exceeds the byte budget, raise Config.MaxStablePrefixBytes or split the prefix
  4. If no stable prefix exists, skip StartEpoch and use Plan's pass-through/observe-only path instead

Example fix

// before
plan, err := eng.StartEpoch(cacheengine.PlanRequest{
    Segments: []cacheengine.Segment{{Text: now(), Stable: false}}, // nothing stable
})

// after
plan, err := eng.StartEpoch(cacheengine.PlanRequest{
    Segments: []cacheengine.Segment{
        {Text: systemPrompt, Stable: true}, // stable head
        {Text: userInput, Stable: false},
    },
})
Defensive patterns

Strategy: validation

Validate before calling

stableBytes := 0
for _, s := range req.Segments {
	if s.Stable {
		stableBytes += len(s.Text)
	}
}
if stableBytes == 0 {
	return errors.New("no stable segments; skip StartEpoch and use Plan pass-through")
}

Type guard

func hasStablePrefix(segments []cacheengine.Segment) bool {
	for _, s := range segments {
		if s.Stable && len(s.Text) > 0 {
			return true
		}
	}
	return false
}

Try / catch

if _, err := eng.StartEpoch(req); err != nil {
	if err.Error() == "cacheengine: no stable prefix" {
		// acceptable: fall back to Plan-only pass-through for this request
		return eng.Plan(req)
	}
	return Plan{}, err
}

Prevention

When it happens

Trigger: Calling StartEpoch where all segments are volatile/unstable, the segments slice is empty, or every candidate stable segment was dropped for exceeding the max stable prefix byte budget (default defaultInputByteLimit when MaxStablePrefixBytes is unset).

Common situations: Prompts where the leading content is timestamps/UUIDs (nothing stable at the head); segment metadata incorrectly labeling content as unstable; huge system prompts exceeding the configured MaxStablePrefixBytes; calling StartEpoch before segments are assembled.

Related errors


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