gastownhall/beads · error

failed to marshal interactions log entry: %w

Error message

failed to marshal interactions log entry: %w

What it means

Append serializes the Entry with a json.Encoder (HTML escaping disabled) before writing, and wraps any Encode failure as "failed to marshal interactions log entry". In practice json.Encoder on a struct almost never fails — this only triggers for values json cannot represent, such as an Extra map containing unsupported types (channels, funcs, or cyclic data). The cause is always in caller-supplied Entry data.

Source

Thrown at internal/audit/audit.go:148

		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
}

// AppendIfEnabled appends only when the optional JSONL sidecar is enabled.
func AppendIfEnabled(e *Entry) (string, error) {
	if !Enabled() {
		return "", fmt.Errorf("audit JSONL sidecar is disabled; set audit.enabled=true or BD_AUDIT_ENABLED=1 to write %s", FileName)
	}
	return Append(e)
}

// LogFieldChange logs a field change (status, assignee, priority, etc.) to the
// optional JSONL sidecar when it is enabled. First-class issue history is

View on GitHub (pinned to 71377f2769)

Solutions

  1. Sanitize Entry.Extra: keep only JSON-safe scalars, strings, slices, and maps.
  2. Convert unsupported values to strings (e.g. err.Error()) before putting them in Extra.
  3. Use json.Marshal on Extra contents in a pre-check to detect problems early.
  4. Inspect the wrapped %w error; it names the offending Go type.

Example fix

// before
entry.Extra = map[string]any{"handler": handlerFunc} // unencodable
audit.Append(entry) // "failed to marshal interactions log entry: json: unsupported type: func()"
// after
entry.Extra = map[string]any{"handler": fmt.Sprintf("%T", handlerFunc)}
audit.Append(entry)
Defensive patterns

Strategy: validation

Validate before calling

if _, err := json.Marshal(entry.Extra); err != nil {
    entry.Extra = map[string]any{"extra_error": err.Error()}
}

Try / catch

if _, err := audit.Append(e); err != nil {
    if strings.Contains(err.Error(), "failed to marshal interactions log entry") {
        log.Printf("dropping unencodable audit entry: %v", err)
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Populating Entry.Extra with map[string]any values containing channels, functions, complex numbers, or cyclic references; custom json.Marshaler on Extra values returning an error; NaN/Inf values in float fields inside Extra.

Common situations: Storing Go error values or callbacks in Extra for debugging; passing untyped config data straight into Extra; marshaling types from libraries without json tags that fail custom marshaling.

Related errors


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