{"record":{"id":"3573bcb55b18a6ae","repo":"charmbracelet/crush","slug":"message-content-is-shorter-than-read-bytes-d-3573bc","errorCode":null,"errorMessage":"message content is shorter than read bytes: %d < %d","messagePattern":"message content is shorter than read bytes: (.+?) < (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/cmd/run.go","lineNumber":370,"sourceCode":"\t\t}\n\t}\n\tswitch e := ev.(type) {\n\tcase pubsub.Event[proto.Message]:\n\t\tmsg := e.Payload\n\t\tif msg.SessionID != s.sessionID || msg.Role != proto.Assistant || len(msg.Parts) == 0 {\n\t\t\treturn false, nil\n\t\t}\n\t\tif s.runID != \"\" {\n\t\t\treturn false, nil\n\t\t}\n\t\tstop()\n\n\t\tcontent := msg.Content().String()\n\t\treadBytes := s.read[msg.ID]\n\t\tif len(content) < readBytes {\n\t\t\tslog.Error(\"Non-interactive: message content shorter than read bytes\",\n\t\t\t\t\"message_length\", len(content), \"read_bytes\", readBytes)\n\t\t\treturn false, fmt.Errorf(\"message content is shorter than read bytes: %d < %d\", len(content), readBytes)\n\t\t}\n\n\t\tpart := content[readBytes:]\n\t\tif readBytes == 0 {\n\t\t\tpart = strings.TrimLeft(part, \" \\t\")\n\t\t}\n\t\tif s.printed || strings.TrimSpace(part) != \"\" {\n\t\t\ts.printed = true\n\t\t\tfmt.Fprint(s.out, part)\n\t\t}\n\t\ts.read[msg.ID] = len(content)\n\t\treturn false, nil\n\n\tcase pubsub.Event[proto.RunComplete]:\n\t\t// RunComplete is the authoritative end-of-run signal. We\n\t\t// exit on it instead of guessing from message finish parts,\n\t\t// which fire on every tool-call step too and were the\n\t\t// source of the regression where `crush run` exited","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/cmd/run.go#L352-L388","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// guard the stream against shrunk content\ncontent := msg.Content().String()\nreadBytes := s.read[msg.ID]\nif len(content) < readBytes {\n\t// don't error on provider restarts; reset the read cursor instead\n\ts.read[msg.ID] = 0\n\treadBytes = 0\n}\npart := content[readBytes:]","handlingStrategy":"type-guard","validationCode":"// before appending, clamp the read cursor defensively\nreadBytes := s.read[msg.ID]\nif readBytes > len(content) {\n\tslog.Warn(\"Stream content shrank; resetting cursor\", \"msg\", msg.ID)\n\treadBytes = 0\n\ts.read[msg.ID] = 0\n}","typeGuard":"func streamStateValid(content string, readBytes int) bool {\n\treturn readBytes <= len(content)\n}","tryCatchPattern":"part, err := nextChunk(content, s.read[msg.ID])\nif err != nil {\n\tslog.Error(\"Stream desync\", \"err\", err)\n\treturn true, fmt.Errorf(\"stream desync for message %s: %w\", msg.ID, err)\n}","preventionTips":["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"],"tags":["go","streaming","race","consistency"],"backgroundTag":"stream-content-regression","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}