charmbracelet/crush · error

message content is shorter than read bytes: %d < %d

Error message

message content is shorter than read bytes: %d < %d

What it means

Sanity check inside runStream.handle(): when a message update arrives, the accumulated content is shorter than the number of bytes previously streamed to stdout (s.read[msg.ID]), meaning the stream would re-print or corrupt output. It aborts the stream instead of printing wrong data.

Source

Thrown at internal/cmd/run.go:370

		}
	}
	switch e := ev.(type) {
	case pubsub.Event[proto.Message]:
		msg := e.Payload
		if msg.SessionID != s.sessionID || msg.Role != proto.Assistant || len(msg.Parts) == 0 {
			return false, nil
		}
		if s.runID != "" {
			return false, nil
		}
		stop()

		content := msg.Content().String()
		readBytes := s.read[msg.ID]
		if len(content) < readBytes {
			slog.Error("Non-interactive: message content shorter than read bytes",
				"message_length", len(content), "read_bytes", readBytes)
			return false, fmt.Errorf("message content is shorter than read bytes: %d < %d", len(content), readBytes)
		}

		part := content[readBytes:]
		if readBytes == 0 {
			part = strings.TrimLeft(part, " \t")
		}
		if s.printed || strings.TrimSpace(part) != "" {
			s.printed = true
			fmt.Fprint(s.out, part)
		}
		s.read[msg.ID] = len(content)
		return false, nil

	case pubsub.Event[proto.RunComplete]:
		// RunComplete is the authoritative end-of-run signal. We
		// exit on it instead of guessing from message finish parts,
		// which fire on every tool-call step too and were the
		// source of the regression where `crush run` exited

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Update crush — stream reconciliation for out-of-order events has fixes
  2. Ensure only one run writes to a session at a time (new RunID per run as the code does)
  3. Check the provider for regeneration/retry behavior that replaces partial content
  4. If reproducible, capture verbose logs and file an issue with the message ids

Example fix

// guard the stream against shrunk content
content := msg.Content().String()
readBytes := s.read[msg.ID]
if len(content) < readBytes {
	// don't error on provider restarts; reset the read cursor instead
	s.read[msg.ID] = 0
	readBytes = 0
}
part := content[readBytes:]
Defensive patterns

Strategy: type-guard

Validate before calling

// before appending, clamp the read cursor defensively
readBytes := s.read[msg.ID]
if readBytes > len(content) {
	slog.Warn("Stream content shrank; resetting cursor", "msg", msg.ID)
	readBytes = 0
	s.read[msg.ID] = 0
}

Type guard

func streamStateValid(content string, readBytes int) bool {
	return readBytes <= len(content)
}

Try / catch

part, err := nextChunk(content, s.read[msg.ID])
if err != nil {
	slog.Error("Stream desync", "err", err)
	return true, fmt.Errorf("stream desync for message %s: %w", msg.ID, err)
}

Prevention

When it happens

Trigger: A message event for the same message id delivers content shorter than what was already emitted — e.g. out-of-order or replaced message events, a truncated/edited final message from the reconcile path, or duplicate message ids across runs.

Common situations: Provider reconnects and restarts the message content from scratch; pubsub fan-in delivers an older snapshot after partial streaming; race between UpdateMessage and RunComplete reconcile; concurrent runs sharing a session id.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/3573bcb55b18a6ae. Report an issue: GitHub.