{"record":{"id":"56521d9ff6de623e","repo":"plandex-ai/plandex","slug":"error-reading-convo-dir-v","errorCode":null,"errorMessage":"error reading convo dir: %v","messagePattern":"error reading convo dir: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/convo_helpers.go","lineNumber":32,"sourceCode":"\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/google/uuid\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc GetPlanConvo(orgId, planId string) ([]*ConvoMessage, error) {\n\tvar convo []*ConvoMessage\n\tconvoDir := getPlanConversationDir(orgId, planId)\n\n\tfiles, err := os.ReadDir(convoDir)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn convo, nil\n\t\t}\n\n\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)","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/convo_helpers.go#L14-L50","documentation":"GetPlanConvo lists the plan's conversation directory with os.ReadDir and builds the message list from its JSON files. A missing directory is treated as an empty convo (nil error), so this error means ReadDir failed for a reason other than non-existence — permission, I/O, or the path is not a directory. It wraps the underlying *PathError so the OS message is preserved.","triggerScenarios":"os.ReadDir(convoDir) fails with a non-ENOENT error: permissions revoked on the plans/org data directory, the convo path exists but is a regular file or symlink to nowhere, disk I/O error, or the directory was removed/renamed between the existence check paths by an external process (e.g. manual cleanup, backup job, NFS hiccup).","commonSituations":"Server runs as a different user than the one that created the plan data (data-dir owned by root after a container/image change); an operator deleted or moved plans data directory while the server was running; wrong PLANDELANDX data-dir env config pointing at a file; read-only volume mount after storage remount.","solutions":["Check permissions/ownership on the convo directory path returned in the wrapped error and chown/chmod to the server user","Stat the path to see if it is a file instead of a directory; remove or restore it to a directory","Restore the plans data directory from backup if it was deleted or corrupted","Verify the server's data-dir configuration env var points at the intended storage root","Check disk/volume health (dmesg, mount status) if the underlying error is an I/O error"],"exampleFix":"// operator fix: data dir owned by root after image change\n// before\nls -l /plandex-data/plans/org123/plan456/convo  # owned by root\n// after\nchown -R plandex:plandex /plandex-data/plans","handlingStrategy":"try-catch","validationCode":"if info, err := os.Stat(convoDir); err != nil {\n    // treat as missing convo, but log non-NotExist errors early\n} else if !info.IsDir() {\n    return fmt.Errorf(\"convo path %s is not a directory\", convoDir)\n}","typeGuard":"func isPathErr(err error) (*os.PathError, bool) {\n    var pe *os.PathError\n    if errors.As(err, &pe) {\n        return pe, true\n    }\n    return nil, false\n}","tryCatchPattern":"convo, err := db.GetPlanConvo(orgId, planId)\nif err != nil {\n    if strings.Contains(err.Error(), \"permission denied\") {\n        // alert ops: data dir ownership/permissions wrong\n    }\n    return fmt.Errorf(\"plan convo unavailable: %w\", err)\n}","preventionTips":["Run the server as a single dedicated user and own the entire data dir with it","Never place files at paths the app expects to be directories","Monitor data-dir volume health and mount state","Do not run concurrent cleanup scripts against live plan storage"],"tags":["go","filesystem","io","directory"],"backgroundTag":"directory-read-permission-denied","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}