gastownhall/beads · error

heartbeat %s: %w

Error message

heartbeat %s: %w

What it means

Wraps an error from uw.IssueUseCase().Heartbeat after the issue resolved successfully, during the proxied heartbeat transaction. It means recording the heartbeat failed in the use case/storage layer. The wrapped cause carries the real reason.

Source

Thrown at cmd/bd/heartbeat_proxied_server.go:45

// 0 = lease refreshed; nonzero = the lease is gone (wrong owner, not
// in_progress, closed, reclaimed) and the worker should stop.
func runHeartbeatProxiedServer(ctx context.Context, id string) error {
	if uowProvider == nil {
		return HandleErrorRespectJSON("proxied-server UOW provider not initialized")
	}

	res, err := uow.RunTxEphemeral(ctx, uowProvider, func(ctx context.Context, uw uow.UnitOfWork) (heartbeatProxiedOutcome, error) {
		issue, _, rerr := workapi.GetIssueOrWisp(ctx, workapi.NewUOWDetailSource(uw), id)
		if errors.Is(rerr, storage.ErrNotFound) {
			return heartbeatProxiedOutcome{}, fmt.Errorf("issue %s not found", id)
		}
		if rerr != nil {
			return heartbeatProxiedOutcome{}, fmt.Errorf("resolving %s: %w", id, rerr)
		}
		// Wisps resolve here too and are refused below: the repo verb
		// classifies them ErrNotClaimable ("is ephemeral"), same as classic.
		if herr := uw.IssueUseCase().Heartbeat(ctx, issue.ID, actor); herr != nil {
			return heartbeatProxiedOutcome{}, fmt.Errorf("heartbeat %s: %w", issue.ID, herr)
		}
		return heartbeatProxiedOutcome{id: issue.ID, title: issue.Title}, nil
	})
	if err != nil {
		return HandleErrorRespectJSON("%v", err)
	}

	SetLastTouchedID(res.id)
	return renderHeartbeatSuccess(res.id, res.title)
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. If the wrapped cause says the issue is ephemeral/not claimable, use the classic issue ID instead of a wisp ID.
  2. Check storage connectivity/lock state and retry.
  3. Confirm the actor is valid and permitted to update the issue.

Example fix

// before: heartbeating a wisp
bd heartbeat wisp-abc123
// error: heartbeat wisp-abc123: is ephemeral
// after: heartbeat the classic issue
bd heartbeat bd-123
Defensive patterns

Strategy: validation

Validate before calling

// refuse wisps up front: heartbeat only classic issues
if strings.HasPrefix(id, "wisp-") { return errors.New("cannot heartbeat an ephemeral wisp; use the classic issue id") }

Try / catch

if herr := uw.IssueUseCase().Heartbeat(ctx, issue.ID, actor); herr != nil {
	if errors.Is(herr, workapi.ErrNotClaimable) { /* wisp/ephemeral: surface friendly message */ }
	return fmt.Errorf("heartbeat %s: %w", issue.ID, herr)
}

Prevention

When it happens

Trigger: uw.IssueUseCase().Heartbeat(ctx, issue.ID, actor) returns non-nil inside the heartbeat transaction; also note the comment: wisps that resolve here are refused by the repo verb as ErrNotClaimable ('is ephemeral'), which surfaces through this wrapper.

Common situations: Heartbeating an ephemeral wisp (refused by the repo verb), transaction commit failure, storage lock contention, or actor/permission rejection.

Related errors


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