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` exitedView on GitHub (pinned to 7944b8e522)
Solutions
- Update crush — stream reconciliation for out-of-order events has fixes
- Ensure only one run writes to a session at a time (new RunID per run as the code does)
- Check the provider for regeneration/retry behavior that replaces partial content
- 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
- Dedicate one run per session at a time to avoid cross-run stream races
- Keep RunID correlation (as the code does) to ignore foreign RunCompletes
- Update crush to get out-of-order event reconciliation fixes
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
- failed to start agent processing stream: %w
- agent processing failed: %w
- message content is shorter than read bytes: %d < %d
- unsupported by the running server
- clipboard operations are not supported on this platform
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/3573bcb55b18a6ae.
Report an issue: GitHub.