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

RunNonInteractive streams partial message content to stdout, tracking how many bytes were already printed per message (messageReadBytes). When the final message content is shorter than the bytes already emitted — which would make content[readBytes:] panic — this error is returned instead. It indicates the message content changed/shrank between incremental updates, i.e. an internal consistency violation in message assembly.

Source

Thrown at internal/app/app.go:416

				if errors.Is(result.err, context.Canceled) || errors.Is(result.err, agent.ErrRequestCancelled) {
					slog.Debug("Non-interactive: agent processing cancelled", "session_id", sess.ID)
					return nil
				}
				return fmt.Errorf("agent processing failed: %w", result.err)
			}
			return nil

		case event := <-messageEvents:
			msg := event.Payload
			if msg.SessionID == sess.ID && msg.Role == message.Assistant && len(msg.Parts) > 0 {
				stopSpinner()

				content := msg.Content().String()
				readBytes := messageReadBytes[msg.ID]

				if len(content) < readBytes {
					slog.Error("Non-interactive: message content is shorter than read bytes", "message_length", len(content), "read_bytes", readBytes)
					return fmt.Errorf("message content is shorter than read bytes: %d < %d", len(content), readBytes)
				}

				part := content[readBytes:]
				// Trim leading whitespace. Sometimes the LLM includes leading
				// formatting and intentation, which we don't want here.
				if readBytes == 0 {
					part = strings.TrimLeft(part, " \t")
				}
				// Ignore initial whitespace-only messages.
				if printed || strings.TrimSpace(part) != "" {
					printed = true
					fmt.Fprint(output, part)
				}
				messageReadBytes[msg.ID] = len(content)
			}

		case <-ctx.Done():
			stopSpinner()

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Update crush to the latest version (streaming/assembly fixes land frequently)
  2. Note the provider/model used; try a different model to see if it's provider-specific
  3. File a bug with the provider name and SLOG_LEVEL=debug logs (message_length vs read_bytes)
  4. As a workaround, retry the run; it is usually transient per-response
Defensive patterns

Strategy: try-catch

Try / catch

err := app.RunNonInteractive(ctx, w, prompt, "", "", true, "", false)
if err != nil && strings.Contains(err.Error(), "shorter than read bytes") {
	// internal invariant violation: retry once, then report bug
	if rerr := app.RunNonInteractive(ctx, w, prompt, "", "", true, "", false); rerr != nil {
		return fmt.Errorf("content mismatch persisted; report with debug logs: %w", rerr)
	}
	return nil
}

Prevention

When it happens

Trigger: The tracked readBytes for a message ID exceeds len(msg.Content().String()) on the final assistant message — e.g. message parts were replaced/compacted, content was re-rendered shorter, or a provider returned truncated content after partial streaming.

Common situations: Provider streaming quirks where an interim message is later replaced by shorter final content; bugs after provider protocol changes; reasoning/thinking blocks being dropped from content between updates.

Related errors


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