{"record":{"id":"4b8c8b25ecf52f4e","repo":"gastownhall/beads","slug":"heartbeat-id-must-not-be-empty","errorCode":null,"errorMessage":"Heartbeat: id must not be empty","messagePattern":"Heartbeat: id must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/issue.go","lineNumber":1912,"sourceCode":"// issueops — which is what makes `bd unclaim --if-assignee` behave identically\n// on the proxied-server and embedded backends.\nfunc (u *issueUseCaseImpl) UnclaimIfAssignee(ctx context.Context, id, actor, expectedAssignee string) error {\n\tif id == \"\" {\n\t\treturn fmt.Errorf(\"UnclaimIfAssignee: id must not be empty\")\n\t}\n\tif err := u.issueRepo.UnclaimIssueIfAssignee(ctx, id, actor, expectedAssignee); err != nil {\n\t\treturn fmt.Errorf(\"UnclaimIfAssignee: %w\", err)\n\t}\n\treturn nil\n}\n\n// Heartbeat refreshes the lease on an issue actor holds in_progress. The\n// write touches ONLY the ephemeral leases table (bd-lrgn1), so the caller\n// must run it under uow.RunTxEphemeral's no-Dolt-commit form — a heartbeat\n// mints no Dolt commit and no history in any mode (bd-aq0ql).\nfunc (u *issueUseCaseImpl) Heartbeat(ctx context.Context, id, actor string) error {\n\tif id == \"\" {\n\t\treturn fmt.Errorf(\"Heartbeat: id must not be empty\")\n\t}\n\tif err := u.issueRepo.HeartbeatIssue(ctx, id, actor); err != nil {\n\t\treturn fmt.Errorf(\"Heartbeat: %w\", err)\n\t}\n\treturn nil\n}\n\n// WakeExpiredDefers returns every expired DATED defer to open (see\n// issueops.WakeExpiredDefersInTx) and reports how many permanent issues and\n// wisps woke. The caller owns persistence: commit with a wake message iff\n// issues > 0, and with the ephemeral plain-COMMIT form iff only wisps woke\n// (wisp tables are dolt_ignored, so their wake needs a SQL commit but must\n// mint no version commit).\nfunc (u *issueUseCaseImpl) WakeExpiredDefers(ctx context.Context) (issues, wisps int, err error) {\n\tissues, wisps, err = u.issueRepo.WakeExpiredDefers(ctx)\n\tif err != nil {\n\t\treturn 0, 0, fmt.Errorf(\"WakeExpiredDefers: %w\", err)\n\t}","sourceCodeStart":1894,"sourceCodeEnd":1930,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/issue.go#L1894-L1930","documentation":"Validation error from Heartbeat: the lease-refresh call was made with an empty issue id. Thrown before touching the ephemeral leases table, so the lease is untouched. Heartbeat only works for an issue the actor holds in_progress, and the empty-id case is a pure caller contract violation.","triggerScenarios":"Calling Heartbeat(ctx, \"\", actor) — typically when the id tracked by a long-running worker was lost or never set.","commonSituations":"Background workers whose issue-id state was reset by a restart; passing an empty field from config or a queue message into the heartbeat loop.","solutions":["Ensure the worker persists/propagates the issue id across its lifecycle","Validate the id before entering the heartbeat loop","Stop the heartbeat loop and re-claim the issue if the id is lost"],"exampleFix":"// before\nfor range ticker.C {\n\tuc.Heartbeat(ctx, issueID, actor)\n}\n// after\nfor range ticker.C {\n\tif issueID == \"\" {\n\t\tbreak // lost claim; re-acquire instead\n\t}\n\tuc.Heartbeat(ctx, issueID, actor)\n}","handlingStrategy":"validation","validationCode":"if id == \"\" || actor == \"\" {\n\treturn fmt.Errorf(\"heartbeat requires non-empty issue id and actor\")\n}","typeGuard":null,"tryCatchPattern":"if err := uc.Heartbeat(ctx, id, actor); err != nil {\n\tif strings.Contains(err.Error(), \"id must not be empty\") {\n\t\treturn fmt.Errorf(\"worker lost issue id; stopping heartbeat\")\n\t}\n\treturn err\n}","preventionTips":["Persist the claimed issue id so worker restarts can resume heartbeats","Guard the heartbeat loop: skip/stop when id or actor is empty","Only heartbeat issues your actor holds in_progress"],"tags":["validation","lease","heartbeat"],"backgroundTag":"empty-required-argument","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}