gastownhall/beads · error

failed to open interactions log: %w

Error message

failed to open interactions log: %w

What it means

Append opens .beads/interactions.jsonl with os.OpenFile(O_CREATE|O_WRONLY|O_APPEND) after EnsureFile has created it; if the open fails the error is wrapped as "failed to open interactions log". This happens after validation, so it indicates a filesystem-level problem between creation and open — typically permissions, a race removing the file, or the path becoming invalid. The underlying OS error is preserved via %w.

Source

Thrown at internal/audit/audit.go:137

	if err != nil {
		return "", err
	}

	if e.ID == "" {
		e.ID, err = newID()
		if err != nil {
			return "", err
		}
	}
	if e.CreatedAt.IsZero() {
		e.CreatedAt = time.Now().UTC()
	} else {
		e.CreatedAt = e.CreatedAt.UTC()
	}

	f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) // nolint:gosec // intended permissions
	if err != nil {
		return "", fmt.Errorf("failed to open interactions log: %w", err)
	}
	defer func() { _ = f.Close() }() // Best effort: file close in defer after flush

	// Marshal to a single byte slice and write atomically.
	// Using bufio.NewWriter could split into multiple write() syscalls,
	// which interleave under concurrent O_APPEND and corrupt lines.
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	if err := enc.Encode(e); err != nil {
		return "", fmt.Errorf("failed to marshal interactions log entry: %w", err)
	}
	if _, err := f.Write(buf.Bytes()); err != nil {
		return "", fmt.Errorf("failed to write interactions log entry: %w", err)
	}

	return e.ID, nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check permissions and ownership of .beads/interactions.jsonl and its directory.
  2. Re-run the command — a transient concurrent-rotation race usually resolves on retry.
  3. Verify the path is a regular file, not a directory or broken symlink.
  4. Inspect the wrapped OS error with errors.Is(err, fs.ErrPermission) etc. to pinpoint the cause.
  5. Recreate the audit log: remove the broken file and let EnsureFile create a fresh one.

Example fix

// before
if _, err := audit.AppendIfEnabled(e); err != nil {
    return err // "failed to open interactions log: ... is a directory"
}
// after
if fi, statErr := os.Stat(".beads/interactions.jsonl"); statErr == nil && fi.IsDir() {
    os.RemoveAll(".beads/interactions.jsonl") // let bd recreate it
}
Defensive patterns

Strategy: retry

Validate before calling

if fi, err := os.Stat(".beads/interactions.jsonl"); err == nil && !fi.Mode().IsRegular() {
    return fmt.Errorf("interactions.jsonl is not a regular file")
}

Try / catch

if _, err := audit.AppendIfEnabled(e); err != nil {
    if errors.Is(err, fs.ErrPermission) || strings.Contains(err.Error(), "failed to open interactions log") {
        time.Sleep(50 * time.Millisecond)
        _, err = audit.AppendIfEnabled(e) // one retry for rotation races
    }
    return err
}

Prevention

When it happens

Trigger: File permissions changed between EnsureFile and Append; another process deleted or replaced interactions.jsonl with a directory; the .beads path no longer exists; O_APPEND open blocked by ACLs or immutable flags on the file.

Common situations: Concurrent bd processes cleaning/rotating the audit log while another appends; running with a different uid in a shared clone; misconfigured BEADS_DIR; symlinks pointing to read-only locations.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/ec90672a71e71de6. Report an issue: GitHub.