gastownhall/beads · error

journal: read seq counter: %w

Error message

journal: read seq counter: %w

What it means

readEventsHeadInTx reads bd_events_seq.next_seq as the head (high-water mark) of the events journal, used to compute auto-prune bounds and to page through events. This error wraps a failure of that SELECT, excluding ErrNoRows which is treated as an empty journal (returns 0, nil).

Source

Thrown at internal/storage/issueops/journal_prune.go:249

		if rows[i].Seq != rows[i-1].Seq+1 {
			return i
		}
	}
	return 0
}

// readEventsHeadInTx returns the highest seq the counter has ever assigned.
// Prune never touches the counter, so this is the head of the journal's history
// even when every row has been deleted. A missing counter row means no mutation
// has ever been journaled here, which is a head of 0.
func readEventsHeadInTx(ctx context.Context, tx DBTX) (int64, error) {
	var head int64
	err := tx.QueryRowContext(ctx, "SELECT next_seq FROM bd_events_seq WHERE id = 0").Scan(&head)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, nil
	}
	if err != nil {
		return 0, fmt.Errorf("journal: read seq counter: %w", err)
	}
	return head, nil
}

func readEventsRowsInTx(ctx context.Context, tx DBTX, since int64, limit int) ([]storage.EventsJournalRow, error) {
	// CAST(ts AS CHAR) normalizes the DATETIME to a stable string across drivers.
	q := `SELECT seq, CAST(ts AS CHAR), op, issue_id, actor, issue_json, dep_json, comment_json
	      FROM bd_events_journal WHERE seq > ? ORDER BY seq ASC`
	if limit > 0 {
		q += " LIMIT " + strconv.Itoa(limit)
	}
	rows, err := tx.QueryContext(ctx, q, since)
	if err != nil {
		return nil, fmt.Errorf("journal: read since %d: %w", since, err)
	}
	defer rows.Close()

	var out []storage.EventsJournalRow

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run migrations so bd_events_seq exists (missing table is only skipped for optional tables elsewhere, not here).
  2. Retry the read — transient lock waits or deadlocks on the seq row are the most common wrapped cause.
  3. Verify connectivity and that you are not pointing at a replica/backup missing the journal tables.
  4. Check the DB user has SELECT on bd_events_seq.

Example fix

// before
head, err := store.ReadEventsPageInTx(ctx, tx, since, limit) // fails against pre-migration DB
// after
if err := store.Migrate(ctx); err != nil { return err } // ensure bd_events_seq exists before journal reads
head, err := store.ReadEventsPageInTx(ctx, tx, since, limit)
Defensive patterns

Strategy: try-catch

Validate before calling

var exists int
err := db.QueryRow("SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'bd_events_seq'").Scan(&exists)
if err != nil || exists == 0 {
    return errors.New("run migrations: bd_events_seq missing")
}

Try / catch

page, err := store.ReadEventsPageInTx(ctx, tx, since, limit)
if err != nil && strings.Contains(err.Error(), "journal: read seq counter") {
    if isTransient(err) { return retryOperation() }
    return fmt.Errorf("journal unreadable, check migration state: %w", err)
}

Prevention

When it happens

Trigger: Calling ComputeEventsAutoPruneBoundInTx or ReadEventsPageInTx when the SELECT from bd_events_seq fails with a real driver error: lost connection, locked table, missing table without migration, or permission denial on SELECT.

Common situations: DB connection dropped between calls; a long-running writer transaction holding locks on bd_events_seq; opening a pre-migration database; read-only replica lacking the table.

Related errors


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