plandex-ai/plandex · error

error reading convo dir: %v

Error message

error reading convo dir: %v

What it means

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.

Source

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

	shared "plandex-shared"

	"github.com/fatih/color"
	"github.com/google/uuid"
	"github.com/sashabaranov/go-openai"
)

func GetPlanConvo(orgId, planId string) ([]*ConvoMessage, error) {
	var convo []*ConvoMessage
	convoDir := getPlanConversationDir(orgId, planId)

	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)

View on GitHub (pinned to e2d772072e)

Solutions

  1. Check permissions/ownership on the convo directory path returned in the wrapped error and chown/chmod to the server user
  2. Stat the path to see if it is a file instead of a directory; remove or restore it to a directory
  3. Restore the plans data directory from backup if it was deleted or corrupted
  4. Verify the server's data-dir configuration env var points at the intended storage root
  5. Check disk/volume health (dmesg, mount status) if the underlying error is an I/O error

Example fix

// operator fix: data dir owned by root after image change
// before
ls -l /plandex-data/plans/org123/plan456/convo  # owned by root
// after
chown -R plandex:plandex /plandex-data/plans
Defensive patterns

Strategy: try-catch

Validate before calling

if info, err := os.Stat(convoDir); err != nil {
    // treat as missing convo, but log non-NotExist errors early
} else if !info.IsDir() {
    return fmt.Errorf("convo path %s is not a directory", convoDir)
}

Type guard

func isPathErr(err error) (*os.PathError, bool) {
    var pe *os.PathError
    if errors.As(err, &pe) {
        return pe, true
    }
    return nil, false
}

Try / catch

convo, err := db.GetPlanConvo(orgId, planId)
if err != nil {
    if strings.Contains(err.Error(), "permission denied") {
        // alert ops: data dir ownership/permissions wrong
    }
    return fmt.Errorf("plan convo unavailable: %w", err)
}

Prevention

When it happens

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

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

Related errors


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