plandex-ai/plandex · error

error writing convo message: %v

Error message

error writing convo message: %v

What it means

StoreConvoMessage writes the marshalled JSON to <convoDir>/<message.Id>.json with os.WriteFile after successfully creating the directory. This error wraps that write failure; the directory exists but the file could not be created or fully written, so the convo message was not persisted (though the in-memory Id was already assigned).

Source

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

	message.CreatedAt = ts

	bytes, err := json.Marshal(message)

	if err != nil {
		return "", fmt.Errorf("error marshalling convo message: %v", err)
	}

	err = os.MkdirAll(convoDir, os.ModePerm)

	if err != nil {
		return "", fmt.Errorf("error creating convo dir: %v", err)
	}

	err = os.WriteFile(filepath.Join(convoDir, message.Id+".json"), bytes, os.ModePerm)

	if err != nil {
		return "", fmt.Errorf("error writing convo message: %v", err)
	}

	err = AddPlanConvoMessage(message, branch)

	if err != nil {
		return "", fmt.Errorf("error adding convo tokens: %v", err)
	}

	var desc string
	if message.Role == openai.ChatMessageRoleUser {
		desc = "💬 User prompt"
		// TODO: add user name
	} else {
		desc = "🤖 Plandex reply"
		if message.Stopped {
			desc += " | 🛑 " + color.New(color.FgHiRed).Sprint("stopped")
		}
	}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Check the wrapped *PathError: fix permissions (chown/chmod) on the convo directory if EACCES
  2. Free disk space / check quota if the error is ENOSPC or EDQUOT
  3. Ensure message.Id is either empty (it gets a fresh UUID) or a valid UUID before calling StoreConvoMessage
  4. Check volume/disk health (dmesg, smartctl) if the error indicates an I/O error
  5. Retry the store after fixing storage — the message was not committed yet, so a clean retry is safe

Example fix

// before
message.Id = userSuppliedId
err = os.WriteFile(filepath.Join(convoDir, message.Id+".json"), bytes, os.ModePerm)
// after
if message.Id == "" {
    message.Id = uuid.New().String()
}
err = os.WriteFile(filepath.Join(convoDir, message.Id+".json"), bytes, os.ModePerm)
Defensive patterns

Strategy: retry

Validate before calling

convoDir := getPlanConversationDir(orgId, planId)
if info, err := os.Stat(convoDir); err != nil || !info.IsDir() {
    return fmt.Errorf("convo dir not ready: %s", convoDir)
}
if err := syscall.Access(convoDir, os.O_WRONLY); err != nil {
    return fmt.Errorf("convo dir not writable: %w", err)
}
if free, err := diskFree(convoDir); err == nil && free < 10<<20 {
    return fmt.Errorf("low disk: %d bytes free", free)
}

Type guard

func isWritePathError(err error) bool {
    var pe *fs.PathError
    return errors.As(err, &pe) && pe.Op == "open"
}

Try / catch

id, err := db.StoreConvoMessage(repo, msg, userId, branch, true)
if err != nil {
    if strings.Contains(err.Error(), "writing convo message") && isTransient(err) {
        time.Sleep(500 * time.Millisecond)
        id, err = db.StoreConvoMessage(repo, msg, userId, branch, true) // safe: nothing committed yet
    }
    return err
}

Prevention

When it happens

Trigger: os.WriteFile fails: permission denied on the existing convo directory, disk full (ENOSPC) mid-write, message.Id contains path-hostile characters (it shouldn't — it's a UUID — but an injected Id could break the path), I/O error on the storage device, or quota exceeded.

Common situations: Disk/quota exhaustion on busy servers storing many large replies; permission drift after a restore or container image change; NFS/EBS transient I/O errors; a message.Id supplied by a fork/plugin that is empty or contains a '/' creating an invalid path.

Related errors


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