gastownhall/beads · warning · storage.ErrNotClaimable
%w: %s is ephemeral
Error message
%w: %s is ephemeral
What it means
HeartbeatIssue refreshes an actor's lease on an in_progress issue, but wisps (ephemeral issues) are never leased and cannot be heartbeaten. If the target id is an active wisp, the call returns storage.ErrNotClaimable wrapped with '<id> is ephemeral'. This is a sentinel-wrapped error — callers should test with errors.Is(err, storage.ErrNotClaimable).
Source
Thrown at internal/storage/embeddeddolt/issues.go:121
if err := issueops.CheckVersionInTx(ctx, tx, id, *opts.ExpectedVersion); err != nil {
return err
}
}
if err := issueops.CheckExpectedFieldsInTx(ctx, tx, id, opts.ExpectedAssignee, opts.ExpectedStatus); err != nil {
return err
}
_, err := issueops.UpdateIssueInTx(ctx, tx, id, updates, actor)
return err
})
}
// HeartbeatIssue refreshes the lease on an issue actor holds in_progress.
// Delegates SQL work to issueops; EmbeddedDolt auto-commits the transaction.
func (s *EmbeddedDoltStore) HeartbeatIssue(ctx context.Context, id, actor string) error {
return s.withConn(ctx, true, func(tx *sql.Tx) error {
if issueops.IsActiveWispInTx(ctx, tx, id) {
// Wisps are ephemeral and never leased; nothing to heartbeat.
return fmt.Errorf("%w: %s is ephemeral", storage.ErrNotClaimable, id)
}
return issueops.HeartbeatIssueInTx(ctx, tx, id, actor)
})
}
// ReclaimExpiredLeases reverts in_progress issues whose lease expired more than
// olderThan ago back to ready, recovering work stranded by dead workers.
func (s *EmbeddedDoltStore) ReclaimExpiredLeases(ctx context.Context, olderThan time.Duration, filter types.ReclaimFilter, actor string) ([]types.ReclaimedLease, error) {
cutoff := time.Now().UTC().Add(-olderThan)
var reclaimed []types.ReclaimedLease
err := s.withConn(ctx, true, func(tx *sql.Tx) error {
var err error
reclaimed, err = issueops.ReclaimExpiredLeasesInTx(ctx, tx, cutoff, filter, actor)
return err
})
return reclaimed, err
}
View on GitHub (pinned to 71377f2769)
Solutions
- Check errors.Is(err, storage.ErrNotClaimable) and treat wisps as lease-free — skip heartbeating for them.
- Promote/resolve the wisp instead of heartbeating if the work is ongoing.
- Track whether the claimed id came from a wisp-producing flow and bypass the heartbeat loop for those.
- Upgrade client code that predates wisps to use the claim API's wisp awareness.
Example fix
// before: blind heartbeat loop fails on wisps
if err := store.HeartbeatIssue(ctx, id, actor); err != nil {
return err
}
// after: treat ErrNotClaimable as skip, not fatal
if err := store.HeartbeatIssue(ctx, id, actor); err != nil {
if errors.Is(err, storage.ErrNotClaimable) {
return nil // ephemeral wisp: no lease to renew
}
return err
} Defensive patterns
Strategy: type-guard
Validate before calling
// skip heartbeats for wisps up front
func isEphemeralWisp(store *embeddeddolt.EmbeddedDoltStore, ctx context.Context, id string) bool {
return issueops.IsActiveWisp(ctx, store.DB(), id)
} Type guard
func notClaimable(err error) bool {
return errors.Is(err, storage.ErrNotClaimable)
} Try / catch
err := store.HeartbeatIssue(ctx, id, actor)
switch {
case err == nil:
// lease renewed
case errors.Is(err, storage.ErrNotClaimable):
// ephemeral wisp: no lease to renew, not a failure
err = nil
default:
return err
} Prevention
- Track whether a claimed id originated from a wisp-producing flow.
- Use errors.Is against storage.ErrNotClaimable, never string matching.
- Promote wisps to durable issues before running lease loops.
- Update legacy heartbeat loops to be wisp-aware.
When it happens
Trigger: Calling store.HeartbeatIssue(ctx, id, actor) where issueops.IsActiveWispInTx reports the id is an active wisp — i.e. the agent heartbeating the claim actually holds a wisp created for ephemeral work, not a durable issue.
Common situations: An agent claiming work via a formula/molecule flow receives a wisp id and then runs a generic heartbeat loop against it; confusion between wisp ids and their promoted issue ids; stale client code written before wisps existed.
Related errors
- %w: %s is ephemeral
- storage.ErrNotClaimable
- db: IssueSQLRepository.HeartbeatIssue: %w
- Heartbeat: id must not be empty
- Heartbeat: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/c9550aef3706bb40.
Report an issue: GitHub.