{"record":{"id":"5344d2438f640c56","repo":"plandex-ai/plandex","slug":"error-unmarshalling-convo-file-v","errorCode":null,"errorMessage":"error unmarshalling convo file: %v","messagePattern":"error unmarshalling convo file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/convo_helpers.go","lineNumber":58,"sourceCode":"\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetPlanConvo: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetPlanConvo: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tbytes, err := os.ReadFile(filepath.Join(convoDir, file.Name()))\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error reading convo file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar convoMessage ConvoMessage\n\t\t\terr = json.Unmarshal(bytes, &convoMessage)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error unmarshalling convo file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconvoCh <- &convoMessage\n\n\t\t}(file)\n\t}\n\n\tfor i := 0; i < len(files); i++ {\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\treturn nil, fmt.Errorf(\"error reading convo files: %v\", err)\n\t\tcase convoMessage := <-convoCh:\n\t\t\tconvo = append(convo, convoMessage)\n\t\t}\n\t}\n\n\tsort.Slice(convo, func(i, j int) bool {","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/convo_helpers.go#L40-L76","documentation":"A convo file was read successfully but json.Unmarshal could not decode it into the ConvoMessage struct. The invalid file is reported through errCh and the whole GetPlanConvo call fails wrapped as 'error reading convo files'. This is a data-integrity error: the stored JSON does not match the current ConvoMessage schema.","triggerScenarios":"os.ReadFile succeeds but the bytes are not valid JSON matching ConvoMessage: zero-length/truncated file from an interrupted write, manually edited file, schema change after a version upgrade (field type changed), or a non-JSON file (editor swap file, .DS_Store-style artifact) dropped into the convo dir.","commonSituations":"Server crash or power loss during StoreConvoMessage leaving a partial JSON file; downgrading/upgrading Plandex so old or new field types no longer unmarshal; operator editing message JSON by hand; backup/restore tooling corrupting files.","solutions":["Identify the corrupt file (wrapped underlying error names the JSON offset/type) and delete or repair it","Validate the file with 'jq . <file>.json' to see the exact JSON syntax problem","Restore the file from backup if it was truncated","Run any available Plandex data-migration for plans after a version upgrade","Add schema-defaulting (e.g. disallowunknownfields off, per-field pointers) if upgrading across schema changes"],"exampleFix":"// inspect corrupt file\n// before\njson.Unmarshal(bytes, &convoMessage)\n// after\nif err := json.Unmarshal(bytes, &convoMessage); err != nil {\n    log.Printf(\"corrupt convo file %s: %v\", file.Name(), err)\n}\nif err := json.Unmarshal(bytes, &convoMessage); err != nil {\n    errCh <- fmt.Errorf(\"error unmarshalling convo file: %v\", err)\n}","handlingStrategy":"validation","validationCode":"info, err := os.Stat(path)\nif err == nil && info.Size() == 0 {\n    return fmt.Errorf(\"truncated convo file: %s\", path)\n}\nvar probe map[string]any\nif data, err := os.ReadFile(path); err == nil {\n    if err := json.Unmarshal(data, &probe); err != nil {\n        return fmt.Errorf(\"corrupt convo file %s: %w\", path, err)\n    }\n}","typeGuard":"func isConvoMessage(b []byte) bool {\n    var m db.ConvoMessage\n    return json.Unmarshal(b, &m) == nil && m.Id != \"\"\n}","tryCatchPattern":"convo, err := db.GetPlanConvo(orgId, planId)\nif err != nil {\n    if strings.Contains(err.Error(), \"unmarshalling convo file\") {\n        // locate corrupt file, back it up, remove it, retry\n        quarantineCorruptConvoFiles(convoDir)\n        convo, err = db.GetPlanConvo(orgId, planId)\n    }\n    return err\n}","preventionTips":["Never hand-edit message JSON in the data dir","Write files atomically (temp file + rename) when forking the storage code","Run data migrations after Plandex upgrades before serving traffic","Validate backups restore to parseable JSON","Monitor for zero-length files after server crashes"],"tags":["go","json","unmarshal","data-corruption"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}