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 isView on GitHub (pinned to 71377f2769)
Solutions
- Sanitize Entry.Extra: keep only JSON-safe scalars, strings, slices, and maps.
- Convert unsupported values to strings (e.g. err.Error()) before putting them in Extra.
- Use json.Marshal on Extra contents in a pre-check to detect problems early.
- 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
- Only put JSON-encodable values (strings, numbers, bools, maps, slices) in Extra.
- Convert errors/functions to strings with fmt.Sprintf before storing.
- Round-trip Extra through json.Marshal in tests to catch bad values early.
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
- failed to marshal backup state: %w
- failed to marshal issue %s: %w
- marshal create request: %w
- serializing waits-for metadata: %w
- serializing waits-for also_blocks metadata: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/0305d29954c534e1.
Report an issue: GitHub.