{"record":{"id":"b1b46d2725deb0bb","repo":"gastownhall/beads","slug":"heartbeat-s-w","errorCode":null,"errorMessage":"heartbeat %s: %w","messagePattern":"heartbeat (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/heartbeat_proxied_server.go","lineNumber":45,"sourceCode":"// 0 = lease refreshed; nonzero = the lease is gone (wrong owner, not\n// in_progress, closed, reclaimed) and the worker should stop.\nfunc runHeartbeatProxiedServer(ctx context.Context, id string) error {\n\tif uowProvider == nil {\n\t\treturn HandleErrorRespectJSON(\"proxied-server UOW provider not initialized\")\n\t}\n\n\tres, err := uow.RunTxEphemeral(ctx, uowProvider, func(ctx context.Context, uw uow.UnitOfWork) (heartbeatProxiedOutcome, error) {\n\t\tissue, _, rerr := workapi.GetIssueOrWisp(ctx, workapi.NewUOWDetailSource(uw), id)\n\t\tif errors.Is(rerr, storage.ErrNotFound) {\n\t\t\treturn heartbeatProxiedOutcome{}, fmt.Errorf(\"issue %s not found\", id)\n\t\t}\n\t\tif rerr != nil {\n\t\t\treturn heartbeatProxiedOutcome{}, fmt.Errorf(\"resolving %s: %w\", id, rerr)\n\t\t}\n\t\t// Wisps resolve here too and are refused below: the repo verb\n\t\t// classifies them ErrNotClaimable (\"is ephemeral\"), same as classic.\n\t\tif herr := uw.IssueUseCase().Heartbeat(ctx, issue.ID, actor); herr != nil {\n\t\t\treturn heartbeatProxiedOutcome{}, fmt.Errorf(\"heartbeat %s: %w\", issue.ID, herr)\n\t\t}\n\t\treturn heartbeatProxiedOutcome{id: issue.ID, title: issue.Title}, nil\n\t})\n\tif err != nil {\n\t\treturn HandleErrorRespectJSON(\"%v\", err)\n\t}\n\n\tSetLastTouchedID(res.id)\n\treturn renderHeartbeatSuccess(res.id, res.title)\n}\n","sourceCodeStart":27,"sourceCodeEnd":56,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/heartbeat_proxied_server.go#L27-L56","documentation":"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.","triggerScenarios":"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.","commonSituations":"Heartbeating an ephemeral wisp (refused by the repo verb), transaction commit failure, storage lock contention, or actor/permission rejection.","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."],"exampleFix":"// before: heartbeating a wisp\nbd heartbeat wisp-abc123\n// error: heartbeat wisp-abc123: is ephemeral\n// after: heartbeat the classic issue\nbd heartbeat bd-123","handlingStrategy":"validation","validationCode":"// refuse wisps up front: heartbeat only classic issues\nif strings.HasPrefix(id, \"wisp-\") { return errors.New(\"cannot heartbeat an ephemeral wisp; use the classic issue id\") }","typeGuard":null,"tryCatchPattern":"if herr := uw.IssueUseCase().Heartbeat(ctx, issue.ID, actor); herr != nil {\n\tif errors.Is(herr, workapi.ErrNotClaimable) { /* wisp/ephemeral: surface friendly message */ }\n\treturn fmt.Errorf(\"heartbeat %s: %w\", issue.ID, herr)\n}","preventionTips":["Only heartbeat classic issue IDs, never wisp IDs.","Keep transactions short to avoid commit failures.","Validate the actor identity before issuing heartbeats from automation."],"tags":["heartbeat","storage","wrapped-error","ephemeral"],"backgroundTag":"heartbeat-update-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}