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
- If the wrapped cause says the issue is ephemeral/not claimable, use the classic issue ID instead of a wisp ID.
- Check storage connectivity/lock state and retry.
- 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
- Only heartbeat classic issue IDs, never wisp IDs.
- Keep transactions short to avoid commit failures.
- Validate the actor identity before issuing heartbeats from automation.
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
- resolving %s: %w
- failed to check parent issue: %w
- failed to search issues: %w
- failed to load dependencies: %w
- get molecule children: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/b1b46d2725deb0bb.
Report an issue: GitHub.