gastownhall/beads · error
record event in %s: %w
Error message
record event in %s: %w
What it means
Wraps the INSERT that records the derived event row under its content-derived id in InsertDerivedEventReturningID. ExecContext failure here means the row could not be written — duplicate id (content collision with different disposition), constraint violation, missing table, or transaction already aborted. Callers AddCommentEventInTx and InsertDerivedEvent propagate it.
Source
Thrown at internal/storage/issueops/derivedid.go:158
var id string
if err := rows.Scan(&id); err != nil {
_ = rows.Close()
return "", fmt.Errorf("scan same-content events in %s: %w", table, err)
}
taken[id] = true
}
_ = rows.Close()
if err := rows.Err(); err != nil {
return "", fmt.Errorf("scan same-content events in %s: %w", table, err)
}
id := firstFreeDerivedID(table, digest, taken)
if _, err := tx.ExecContext(ctx, fmt.Sprintf(`
INSERT INTO %s (id, issue_id, event_type, actor, old_value, new_value, comment, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, table),
id,
e.IssueID, string(e.EventType), e.Actor, e.OldValue, e.NewValue, e.Comment, e.CreatedAt); err != nil {
return "", fmt.Errorf("record event in %s: %w", table, err)
}
return id, nil
}
// NextLiveCommentTime returns the created_at to stamp on a comment being added
// live (as opposed to imported), given the wall-clock instant the caller
// observed. The result is always truncated to whole seconds — the created_at
// column's DATETIME(0) precision — and is advanced to one second past the
// issue's newest existing comment when that comment is at or after `now`.
//
// Why: comments read back in (created_at ASC, id ASC) order, created_at holds
// whole seconds, and since bd-ri8bd a comment's id is a content digest rather
// than a time-ordered UUIDv7. Two comments added to one issue inside the same
// wall-clock second therefore tie on the primary sort key and then order by
// hash — arbitrarily with respect to the order they were written. Keeping
// (issue_id, created_at) unique on the live path is what restores insertion
// order for the reader without putting ordering information into the id, which
// content-derivation cannot carry.View on GitHub (pinned to 71377f2769)
Solutions
- Retry the operation in a fresh transaction — if the conflicting id now exists, the dedup SELECT will see it and pick the next free ordinal.
- Serialize concurrent writers on the same issue (bd's locking) instead of racing inserts from multiple processes.
- Verify the table exists with the expected 8 columns; run migrations if not.
- Inspect the wrapped driver error to distinguish PK-duplicate from connection/abort causes.
Example fix
// before: racing inserts collide on derived id
if _, err := tx.ExecContext(ctx, insertSQL, id, ...); err != nil {
return "", fmt.Errorf("record event in %s: %w", table, err)
}
// after: re-read taken set and pick next free id on duplicate-key
if _, err := tx.ExecContext(ctx, insertSQL, id, ...); err != nil {
if isDuplicateKey(err) {
taken[id] = true
id = firstFreeDerivedID(table, digest, taken)
_, err = tx.ExecContext(ctx, insertSQL, id, ...)
}
if err != nil { return "", fmt.Errorf("record event in %s: %w", table, err) }
} Defensive patterns
Strategy: retry
Validate before calling
cols, err := tableColumns(tx, table)
if err != nil { return err }
if len(cols) != 8 {
return fmt.Errorf("%s expects 8 columns, found %d; migrate", table, len(cols))
}
if err := ctx.Err(); err != nil { return err } Type guard
func isDuplicateKeyErr(err error) bool {
var me *mysql.MySQLError
return errors.As(err, &me) && me.Number == 1062
} Try / catch
id, err := InsertDerivedEventReturningID(ctx, tx, table, e)
if err != nil {
if isDuplicateKeyErr(err) {
// benign race: retry in a fresh tx; dedup SELECT will see the row
return insertInFreshTx(ctx, table, e)
}
return err
} Prevention
- Avoid multiple processes writing identical events concurrently; use bd's locking.
- Keep transactions short so they aren't reaped mid-INSERT.
- Migrate schemas before bulk import runs.
- Treat duplicate-key on derived ids as dedup-won, not corruption.
When it happens
Trigger: Two events compute the same derived id via firstFreeDerivedID but race within conflicting transactions; the events table lacks expected columns; the transaction was already rolled back by an earlier error; UNIQUE/PK violation on id.
Common situations: Concurrent writers inserting identical comments through separate processes against the same Dolt DB; schema drift; long-running transactions that get killed server-side before the INSERT.
Related errors
- scan same-content events in %s: %w
- add comment to %s: %w
- iter issues: query: %w
- resolve existing wisps for batch delete: %w
- set repo mtime: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/79c94d041f699b6c.
Report an issue: GitHub.