gastownhall/beads · error
db: IssueSQLRepository.HeartbeatIssue: %w
Error message
db: IssueSQLRepository.HeartbeatIssue: %w
What it means
Wraps a failure from issueops.HeartbeatIssueInTx when renewing an issue lease. Unlike the ErrNotClaimable case, this is an unexpected failure: the heartbeat UPDATE (touching the lease timestamp for the actor) failed at the SQL or validation level. The wrapper adds repository context; the cause is in the chain.
Source
Thrown at internal/storage/domain/db/issue.go:1248
}
return nil
}
// HeartbeatIssue refreshes the lease on an issue actor holds in_progress,
// mirroring DoltStore.HeartbeatIssue: wisps are ephemeral and never leased,
// and the SQL work is the classic issueops.HeartbeatIssueInTx — same clock
// (time.Now().UTC()), same TTL resolution (issueops.LeaseTTL), and the same
// only-current-owner classification (storage.ErrAlreadyClaimed /
// ErrNotClaimable) — so classic `bd reclaim` staleness semantics see proxied
// heartbeats identically. Deliberately NO Dolt commit: the leases table is
// dolt_ignored (bd-lrgn1), and the cmd layer commits this transaction with
// uow.RunTxEphemeral (plain SQL COMMIT, nothing in dolt_log).
func (r *issueSQLRepositoryImpl) HeartbeatIssue(ctx context.Context, id, actor string) error {
if issueops.IsActiveWispInTx(ctx, r.runner, id) {
return fmt.Errorf("db: IssueSQLRepository.HeartbeatIssue: %w: %s is ephemeral", storage.ErrNotClaimable, id)
}
if err := issueops.HeartbeatIssueInTx(ctx, r.runner, id, actor); err != nil {
return fmt.Errorf("db: IssueSQLRepository.HeartbeatIssue: %w", err)
}
return nil
}
// WakeExpiredDefers runs the shared lazy defer-wake body against this
// repository's runner (the same DBTX-shaped seam ReclaimExpiredLeases uses)
// and reports how many rows woke per table. The issues count decides whether
// the transaction's owner mints a dolt commit; the wisps count decides
// whether it must still issue a plain SQL commit — wisp tables are
// dolt_ignored, so a wisp-only wake mints no version commit, but a caller
// that treats it as "nothing happened" rolls the wisp writes back.
func (r *issueSQLRepositoryImpl) WakeExpiredDefers(ctx context.Context) (issues, wisps int, err error) {
out, err := issueops.WakeExpiredDefersInTx(ctx, r.runner)
if err != nil {
return 0, 0, fmt.Errorf("db: IssueSQLRepository.WakeExpiredDefers: %w", err)
}
return len(out.Issues), len(out.Wisps), nil
}View on GitHub (pinned to 71377f2769)
Solutions
- Check the wrapped cause: if the lease is gone, re-claim the issue instead of heartbeating.
- Verify the actor string matches the one used at Claim time.
- Retry transient driver errors; heartbeat is idempotent while the lease exists.
- Tighten the heartbeat interval so renewals arrive before lease expiry.
Example fix
// before
if err := repo.HeartbeatIssue(ctx, id, actor); err != nil { return err }
// after
if err := repo.HeartbeatIssue(ctx, id, actor); err != nil {
return fmt.Errorf("heartbeat %s: %w (lease may be expired — re-claim)", id, err)
} Defensive patterns
Strategy: retry
Validate before calling
// ensure a live claim exists for this actor before renewing
issue, err := repo.Get(ctx, id)
if err != nil { return err }
if issue.Assignee != actor { return fmt.Errorf("no active lease for %s", actor) } Try / catch
if err := repo.HeartbeatIssue(ctx, id, actor); err != nil {
if isLeaseGone(err) { return reClaimIssue(ctx, id, actor) } // expired: re-claim
if isTransientDBErr(err) { return retryHeartbeat(err) }
return err
} Prevention
- Heartbeat at intervals well below the lease TTL.
- Keep the actor string identical to the Claim call.
- On failure, fall back to re-claiming rather than assuming the lease persists.
- Monitor heartbeat error rates to catch clock/TTL misconfig.
When it happens
Trigger: Calling HeartbeatIssue on a non-wisp ID when: the lease row for (id, actor) is missing (lease expired or never claimed), the UPDATE fails at the driver level, or the transaction runner errors.
Common situations: Lease expired and was reclaimed before the heartbeat arrived (stale worker), wrong actor string passed so no matching lease row, connection loss during the write, or DB read-only/permission issue.
Related errors
- %w: %s is ephemeral
- db: Update %s: clear lease: %w
- db: IssueSQLRepository.UnclaimIssue: %w
- db: IssueSQLRepository.ReclaimExpiredLeases: %w
- Heartbeat: id must not be empty
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/458d83735e57aba4.
Report an issue: GitHub.