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

  1. Unwrap the error to see if the lease is missing/expired vs. a storage failure
  2. Re-claim the issue (start it again) if the lease expired, then resume heartbeating
  3. Verify the call runs under uow.RunTxEphemeral's no-Dolt-commit form
  4. 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

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


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