{"record":{"id":"9f48a3f9351e6c4c","repo":"plandex-ai/plandex","slug":"panic-in-getplanconvo-v-n-s","errorCode":null,"errorMessage":"panic in GetPlanConvo: %v\\n%s","messagePattern":"panic in GetPlanConvo: (.+?)\\\\n(.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/convo_helpers.go","lineNumber":43,"sourceCode":"\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)\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","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/convo_helpers.go#L25-L61","documentation":"Each per-file goroutine in GetPlanConvo is wrapped in a recover() so a panic while processing one convo file does not crash the server. The recovered panic value and stack trace are logged and converted into this error, which is then funneled to the caller through errCh. It indicates a bug or malformed input inside the file-processing goroutine, not an OS-level failure.","triggerScenarios":"Any panic inside the goroutine body or its callees during os.ReadFile/Unmarshal handling — e.g. index-out-of-range on a nil field elsewhere, nil pointer dereference in ConvoMessage construction, or a future code change inside the loop that panics on unusual file content (very large file, zero-length file, weird file name).","commonSituations":"Corrupt or adversarial files dropped into the convo dir by an operator; a regression after upgrading Plandex that dereferences a nil field of ConvoMessage; concurrency bug (double channel send) that a developer partially fixed — the Goexit call exists precisely to prevent the double-send.","solutions":["Read the stack trace printed after 'panic in GetPlanConvo:' in the server log to find the panicking line","Inspect/remove the offending file in the convo directory (its name is usually identifiable from context)","Upgrade Plandex to a version fixing the panic, or patch the panicking code path to handle nil/malformed data","If it recurs, add a nil/type check before the operation shown in the stack","Retry GetPlanConvo after removing the bad file — the error is per-file, not per-plan-storage"],"exampleFix":"// harden the processing body against nil data\n// before\nconvoCh <- &convoMessage\n// after\nif convoMessage.Id == \"\" {\n    log.Printf(\"skipping convo file with no id: %s\", file.Name())\n    return\n}\nconvoCh <- &convoMessage","handlingStrategy":"try-catch","validationCode":"files, err := os.ReadDir(convoDir)\nif err == nil {\n    for _, f := range files {\n        if fi, err := f.Info(); err == nil && fi.Size() == 0 {\n            log.Printf(\"warning: empty convo file %s\", f.Name())\n        }\n    }\n}","typeGuard":"func isRecoveredPanicError(err error) bool {\n    return strings.HasPrefix(err.Error(), \"panic in GetPlanConvo:\")\n}","tryCatchPattern":"convo, err := db.GetPlanConvo(orgId, planId)\nif err != nil {\n    if isRecoveredPanicError(err) {\n        // grab the stack trace embedded in the error and file a bug;\n        // retry once after isolating the bad file\n        return retryGetPlanConvo(orgId, planId)\n    }\n    return err\n}","preventionTips":["Keep nil-checks on every ConvoMessage field used in display paths","Add fuzz/unit tests that feed malformed JSON files through the convo reader","Keep panic stacks from server logs (they are embedded in this error) for bug reports","Pin Plandex versions and review changelogs for ConvoMessage schema changes"],"tags":["go","panic","goroutine","concurrency"],"backgroundTag":"goroutine-panic-recovered","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"}