gastownhall/beads · error
Heartbeat: %w
Error message
Heartbeat: %w
What it means
Wrapped error from Heartbeat: issueRepo.HeartbeatIssue failed and is returned with a "Heartbeat: " prefix. The write touches only the ephemeral leases table, so failures usually mean the lease does not exist or the storage/transaction context is wrong — it never mints a Dolt commit.
Source
Thrown at internal/storage/domain/issue.go:1915
if id == "" {
return fmt.Errorf("UnclaimIfAssignee: id must not be empty")
}
if err := u.issueRepo.UnclaimIssueIfAssignee(ctx, id, actor, expectedAssignee); err != nil {
return fmt.Errorf("UnclaimIfAssignee: %w", err)
}
return nil
}
// Heartbeat refreshes the lease on an issue actor holds in_progress. The
// write touches ONLY the ephemeral leases table (bd-lrgn1), so the caller
// must run it under uow.RunTxEphemeral's no-Dolt-commit form — a heartbeat
// mints no Dolt commit and no history in any mode (bd-aq0ql).
func (u *issueUseCaseImpl) Heartbeat(ctx context.Context, id, actor string) error {
if id == "" {
return fmt.Errorf("Heartbeat: id must not be empty")
}
if err := u.issueRepo.HeartbeatIssue(ctx, id, actor); err != nil {
return fmt.Errorf("Heartbeat: %w", err)
}
return nil
}
// WakeExpiredDefers returns every expired DATED defer to open (see
// issueops.WakeExpiredDefersInTx) and reports how many permanent issues and
// wisps woke. The caller owns persistence: commit with a wake message iff
// issues > 0, and with the ephemeral plain-COMMIT form iff only wisps woke
// (wisp tables are dolt_ignored, so their wake needs a SQL commit but must
// mint no version commit).
func (u *issueUseCaseImpl) WakeExpiredDefers(ctx context.Context) (issues, wisps int, err error) {
issues, wisps, err = u.issueRepo.WakeExpiredDefers(ctx)
if err != nil {
return 0, 0, fmt.Errorf("WakeExpiredDefers: %w", err)
}
return issues, wisps, nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Unwrap the error to see if the lease is missing/expired vs. a storage failure
- Re-claim the issue (start it again) if the lease expired, then resume heartbeating
- Verify the call runs under uow.RunTxEphemeral's no-Dolt-commit form
- Confirm actor and id match the original claim holder
Example fix
// before
if err := uc.Heartbeat(ctx, id, actor); err != nil { return err }
// after
if err := uc.Heartbeat(ctx, id, actor); err != nil {
if isLeaseLost(err) {
return reClaimAndResume(ctx, id, actor) // lease expired; restart work
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
if id == "" || actor == "" {
return fmt.Errorf("heartbeat requires non-empty issue id and actor")
} Try / catch
if err := uc.Heartbeat(ctx, id, actor); err != nil {
if isLeaseLost(err) {
return reClaimIssue(ctx, id, actor) // lease expired or taken
}
return fmt.Errorf("heartbeat %s: %w", id, err)
} Prevention
- Heartbeat at an interval well under the lease TTL
- Run the call under uow.RunTxEphemeral's no-Dolt-commit form as documented
- Treat lease-lost as a signal to re-claim, not to keep retrying heartbeats
When it happens
Trigger: Calling Heartbeat(ctx, id, actor) when the actor holds no in_progress lease on the issue, the lease expired and was reclaimed, the call is not run under uow.RunTxEphemeral's no-Dolt-commit form, or a storage error occurs.
Common situations: Heartbeating after the lease expired (slow worker, GC pause); heartbeating an issue claimed by a different actor; calling outside the required ephemeral transaction scope after a refactor.
Related errors
- not found
- load wisp labels: %w
- edge %d %s->%s: checking planned blocking cycle: %w
- reading existing dependencies for %s: %w
- resolving %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/8626f470bfbdb90d.
Report an issue: GitHub.