{"record":{"id":"8626f470bfbdb90d","repo":"gastownhall/beads","slug":"heartbeat-w","errorCode":null,"errorMessage":"Heartbeat: %w","messagePattern":"Heartbeat: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/issue.go","lineNumber":1915,"sourceCode":"\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}\n\treturn issues, wisps, nil\n}\n","sourceCodeStart":1897,"sourceCodeEnd":1933,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/issue.go#L1897-L1933","documentation":"Wrapped error from Heartbeat: issueRepo.HeartbeatIssue failed and is returned with a \"Heartbeat: \" prefix. The write touches only the ephemeral leases table, so failures usually mean the lease does not exist or the storage/transaction context is wrong — it never mints a Dolt commit.","triggerScenarios":"Calling Heartbeat(ctx, id, actor) when the actor holds no in_progress lease on the issue, the lease expired and was reclaimed, the call is not run under uow.RunTxEphemeral's no-Dolt-commit form, or a storage error occurs.","commonSituations":"Heartbeating after the lease expired (slow worker, GC pause); heartbeating an issue claimed by a different actor; calling outside the required ephemeral transaction scope after a refactor.","solutions":["Unwrap the error to see if the lease is missing/expired vs. a storage failure","Re-claim the issue (start it again) if the lease expired, then resume heartbeating","Verify the call runs under uow.RunTxEphemeral's no-Dolt-commit form","Confirm actor and id match the original claim holder"],"exampleFix":"// before\nif err := uc.Heartbeat(ctx, id, actor); err != nil { return err }\n// after\nif err := uc.Heartbeat(ctx, id, actor); err != nil {\n\tif isLeaseLost(err) {\n\t\treturn reClaimAndResume(ctx, id, actor) // lease expired; restart work\n\t}\n\treturn err\n}","handlingStrategy":"try-catch","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 isLeaseLost(err) {\n\t\treturn reClaimIssue(ctx, id, actor) // lease expired or taken\n\t}\n\treturn fmt.Errorf(\"heartbeat %s: %w\", id, err)\n}","preventionTips":["Heartbeat at an interval well under the lease TTL","Run the call under uow.RunTxEphemeral's no-Dolt-commit form as documented","Treat lease-lost as a signal to re-claim, not to keep retrying heartbeats"],"tags":["storage","error-wrapping","lease","heartbeat"],"backgroundTag":"repo-error-wrapping","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}