{"record":{"id":"d318d346061f2c30","repo":"plandex-ai/plandex","slug":"error-reading-convo-file-v","errorCode":null,"errorMessage":"error reading convo file: %v","messagePattern":"error reading convo file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/convo_helpers.go","lineNumber":50,"sourceCode":"\t\treturn nil, fmt.Errorf(\"error reading convo dir: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(files))\n\tconvoCh := make(chan *ConvoMessage, len(files))\n\n\tfor _, file := range files {\n\t\tgo func(file os.DirEntry) {\n\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 {","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/convo_helpers.go#L32-L68","documentation":"A worker goroutine failed to read one of the per-message JSON files in the plan convo directory via os.ReadFile. The error is sent to errCh and surfaced by GetPlanConvo wrapped as 'error reading convo files'. Unlike a missing whole directory, each individual file is expected to exist once ReadDir listed it, so this usually signals a race (file deleted mid-read) or permission problem.","triggerScenarios":"os.ReadFile(filepath.Join(convoDir, file.Name())) returns an error: the file was deleted between ReadDir and ReadFile (concurrent cleanup, plan deletion), permissions deny read, the entry is a broken symlink or special file, or name contains characters that break path resolution.","commonSituations":"Two server processes sharing one data dir where one deletes the plan; a cron/cleanup script purging old convo files while a user is viewing the convo; unreadable files after a restore with wrong ownership; non-regular files (sockets/temp files) accidentally placed in the convo dir.","solutions":["Check whether another process/script is deleting files from the convo dir concurrently and stop it or serialize access","Verify file ownership/permissions in the convo dir (ls -l) and fix with chown/chmod","Remove non-regular entries (symlinks, temp files) from the convo dir","Ensure only one Plandex server instance uses the same data directory","Retry the request — if transient (deleted mid-read), it may succeed after cleanup settles"],"exampleFix":"// skip entries that are not regular files\n// before\nfor _, file := range files {\n    go func(file os.DirEntry) {\n// after\nfor _, file := range files {\n    if !file.Type().IsRegular() {\n        continue\n    }\n    go func(file os.DirEntry) {","handlingStrategy":"retry","validationCode":"for _, f := range mustListDir(convoDir) {\n    if _, err := os.Stat(filepath.Join(convoDir, f.Name())); err != nil {\n        log.Printf(\"convo file vanished between list and read: %s\", f.Name())\n    }\n}","typeGuard":"func isRetryableReadErr(err error) bool {\n    return errors.Is(err, os.ErrPermission) || errors.Is(err, syscall.EIO) || strings.Contains(err.Error(), \"no such file\")\n}","tryCatchPattern":"var convo []*db.ConvoMessage\nvar err error\nfor i := 0; i < 3; i++ {\n    convo, err = db.GetPlanConvo(orgId, planId)\n    if err == nil || !isRetryableReadErr(err) {\n        break\n    }\n    time.Sleep(100 * time.Millisecond << i)\n}","preventionTips":["Run exactly one server instance per data directory","Exclude the plans data dir from external cleanup jobs","Restore files with correct ownership after backups","Keep the convo dir free of symlinks and non-regular files"],"tags":["go","filesystem","io","race-condition"],"backgroundTag":"file-not-found","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}