plandex-ai/plandex · error

panic in GetPlanConvo: %v\n%s

Error message

panic in GetPlanConvo: %v\n%s

What it means

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.

Source

Thrown at app/server/db/convo_helpers.go:43

	files, err := os.ReadDir(convoDir)
	if err != nil {
		if os.IsNotExist(err) {
			return convo, nil
		}

		return nil, fmt.Errorf("error reading convo dir: %v", err)
	}

	errCh := make(chan error, len(files))
	convoCh := make(chan *ConvoMessage, len(files))

	for _, file := range files {
		go func(file os.DirEntry) {
			defer func() {
				if r := recover(); r != nil {
					log.Printf("panic in GetPlanConvo: %v\n%s", r, debug.Stack())
					errCh <- fmt.Errorf("panic in GetPlanConvo: %v\n%s", r, debug.Stack())
					runtime.Goexit() // don't allow outer function to continue and double-send to channel
				}
			}()
			bytes, err := os.ReadFile(filepath.Join(convoDir, file.Name()))

			if err != nil {
				errCh <- fmt.Errorf("error reading convo file: %v", err)
				return
			}

			var convoMessage ConvoMessage
			err = json.Unmarshal(bytes, &convoMessage)

			if err != nil {
				errCh <- fmt.Errorf("error unmarshalling convo file: %v", err)
				return
			}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Read the stack trace printed after 'panic in GetPlanConvo:' in the server log to find the panicking line
  2. Inspect/remove the offending file in the convo directory (its name is usually identifiable from context)
  3. Upgrade Plandex to a version fixing the panic, or patch the panicking code path to handle nil/malformed data
  4. If it recurs, add a nil/type check before the operation shown in the stack
  5. Retry GetPlanConvo after removing the bad file — the error is per-file, not per-plan-storage

Example fix

// harden the processing body against nil data
// before
convoCh <- &convoMessage
// after
if convoMessage.Id == "" {
    log.Printf("skipping convo file with no id: %s", file.Name())
    return
}
convoCh <- &convoMessage
Defensive patterns

Strategy: try-catch

Validate before calling

files, err := os.ReadDir(convoDir)
if err == nil {
    for _, f := range files {
        if fi, err := f.Info(); err == nil && fi.Size() == 0 {
            log.Printf("warning: empty convo file %s", f.Name())
        }
    }
}

Type guard

func isRecoveredPanicError(err error) bool {
    return strings.HasPrefix(err.Error(), "panic in GetPlanConvo:")
}

Try / catch

convo, err := db.GetPlanConvo(orgId, planId)
if err != nil {
    if isRecoveredPanicError(err) {
        // grab the stack trace embedded in the error and file a bug;
        // retry once after isolating the bad file
        return retryGetPlanConvo(orgId, planId)
    }
    return err
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/9f48a3f9351e6c4c. Report an issue: GitHub.