gastownhall/beads · error
journal: snapshot %s for %s: %w
Error message
journal: snapshot %s for %s: %w
What it means
RecordEventInTx snapshots the issue row before inserting a journal event. If snapshotting fails, the mutation and the journal disagree (row missing for a non-delete op), so the transaction is failed rather than recording a journal hole. Only ErrNotFound from getJournalIssueInTx surfaces as missing-row; other errors are wrapped here too.
Source
Thrown at internal/storage/issueops/journal.go:318
// Use it for every op except delete (which has no surviving row — use
// RecordDeleteInTx) and dependency ops (use RecordDepEventInTx). A no-op when
// journaling is disabled.
//
// actor is the acting identity that performed the mutation, as resolved for
// the audit-events table; "" when the mutation path genuinely has none
// (derived maintenance, actorless delete plumbing). It is an explicit
// parameter, not ambient context, so a new call site cannot compile without
// deciding attribution.
func RecordEventInTx(ctx context.Context, tx DBTX, op EventOp, issueID, actor string) error {
if !journalEnabled(ctx, tx) {
return nil
}
issue, err := getJournalIssueInTx(ctx, tx, issueID)
if err != nil {
// The row should exist for a non-delete op; a missing row means the
// mutation and the journal disagree, so fail the transaction rather than
// record a hole.
return fmt.Errorf("journal: snapshot %s for %s: %w", op, issueID, err)
}
return insertEventRow(ctx, tx, op, issueID, issue, nil, nil, actor)
}
// RecordDeleteInTx records a delete for issueID with a null issue payload (the
// row no longer exists). A no-op when journaling is disabled. actor as on
// RecordEventInTx.
func RecordDeleteInTx(ctx context.Context, tx DBTX, issueID, actor string) error {
if !journalEnabled(ctx, tx) {
return nil
}
return insertEventRow(ctx, tx, EventDelete, issueID, nil, nil, nil, actor)
}
// journalableDeletesInTx narrows ids to the ones that actually exist in table,
// so a bulk delete records only rows it really removes. It is a no-op (nil,
// nil) when journaling is disabled, keeping the extra read off the ordinary
// local delete path. Callers MUST invoke it before issuing their DELETE.View on GitHub (pinned to 71377f2769)
Solutions
- Verify the issueID exists before the mutation (or handle delete via RecordDeleteInTx)
- Check for concurrent deletes racing this mutation; serialize ordering
- Inspect the wrapped getJournalIssueInTx cause to distinguish missing-row vs SQL failure
- If the source may legitimately be gone, use RecordDepEventInTx-style null-snapshot handling instead
Example fix
// before
if err := RecordEventInTx(ctx, tx, EventClose, id, actor); err != nil { ... }
// after
var exists int
_ = tx.QueryRow("SELECT 1 FROM issues WHERE id = ?", id).Scan(&exists)
if exists == 0 { return storage.ErrNotFound } // or use RecordDeleteInTx Defensive patterns
Strategy: try-catch
Validate before calling
var exists int
err := db.QueryRow("SELECT 1 FROM issues WHERE id = ?", issueID).Scan(&exists)
if err == sql.ErrNoRows { /* use RecordDeleteInTx instead, or abort */ } Type guard
null
Try / catch
if err := issueops.RecordEventInTx(ctx, tx, EventUpdate, id, actor); err != nil {
if errors.Is(err, storage.ErrNotFound) { return issueops.RecordDeleteInTx(ctx, tx, id, actor) }
return fmt.Errorf("journal event failed: %w", err)
} Prevention
- Verify the issue exists before claim/close/rename mutations
- Use RecordDeleteInTx for deleted rows, not RecordEventInTx
- Avoid concurrent delete + update of the same issue id
When it happens
Trigger: Calling ClaimIssueInTx / closeIssueInTx / CreateIssueInTxWithResult / AddLabelInTx / recordRenameInJournal for an issueID whose row does not exist at journal time (e.g. deleted concurrently), or the snapshot SELECT fails on a SQL/connection error.
Common situations: Double-close or claim of an already-deleted issue; cascade delete removing the row mid-transaction; wrong issue ID passed in; database connectivity problems.
Related errors
- journal dependency removals for batched wisp delete: %w
- journal dependency removals for batch delete: %w
- journal: snapshot derived is_blocked from %s: %w
- journal: record derived is_blocked update for %s: %w
- ErrTransaction
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/68b5abc130a92828.
Report an issue: GitHub.