{"record":{"id":"d212bf768aafede6","repo":"gastownhall/beads","slug":"failed-to-record-unclaim-event-w","errorCode":null,"errorMessage":"failed to record unclaim event: %w","messagePattern":"failed to record unclaim event: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/unclaim.go","lineNumber":128,"sourceCode":"}\n\n// finishUnclaimInTx applies the post-UPDATE half of a release shared by\n// UnclaimIssueInTx and UnclaimIssueIfAssigneeInTx: it drops the lease row (a\n// no-op when none exists, e.g. a wisp or an open-but-assigned issue that was\n// never leased) and records the \"unclaimed\" event. The row mutation\n// (assignee/status/started_at/row_lock) must already have been applied in tx.\nfunc finishUnclaimInTx(ctx context.Context, tx DBTX, eventTable string, id string, actor string, oldIssue *types.Issue) error {\n\tif err := DeleteLeaseInTx(ctx, tx, id); err != nil {\n\t\treturn err\n\t}\n\n\toldData, _ := json.Marshal(oldIssue)\n\tnewData, _ := json.Marshal(map[string]interface{}{\n\t\t\"assignee\": \"\",\n\t\t\"status\":   \"open\",\n\t})\n\tif err := RecordFullEventInTable(ctx, tx, eventTable, id, \"unclaimed\", actor, string(oldData), string(newData)); err != nil {\n\t\treturn fmt.Errorf(\"failed to record unclaim event: %w\", err)\n\t}\n\t// A release changes assignee and status, so it journals as an update. Both\n\t// unclaim entry points funnel through here after their CAS succeeded, so\n\t// this covers the conditional release too.\n\treturn RecordEventInTx(ctx, tx, EventUpdate, id, actor)\n}\n\n// UnclaimIssueIfAssigneeInTx atomically releases a claim only while the issue is\n// still assigned to expectedAssignee — the compare-and-swap inverse of\n// ClaimIssueInTx: a Go-side actorMatches precheck (ga-5ksp5) plus a conditional\n// UPDATE CASed on row_lock, with RowsAffected as the verdict, so a stale\n// releaser can never clobber a claim that has since moved to (or been\n// re-taken by) someone else. \"Still assigned to expectedAssignee\" is judged\n// under actorMatches, not verbatim equality, so a caller naming the current\n// holder under a different layer's spelling of the same identity is a match,\n// not a mismatch — see canonicalActor. On success it applies the same\n// transition as UnclaimIssueInTx (assignee cleared, status reopened,\n// started_at cleared, lease dropped, row_lock rewritten, \"unclaimed\" event","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/unclaim.go#L110-L146","documentation":"After a successful unclaim UPDATE, finishUnclaimInTx journals an 'unclaimed' event via RecordFullEventInTable and this wrapped error propagates if that journaling fails. The row mutation already succeeded inside the transaction; this failure prevents the audit trail from recording the release, so the whole transaction errors rather than committing an unjournaled release.","triggerScenarios":"RecordFullEventInTable fails due to an event-table schema mismatch, a disk-full/IO error in the underlying DB, a constraint violation on the event row, or a canceled context while writing the event.","commonSituations":"Old database missing the richer event columns; event table corrupted or locked by a migration; storage full so the event insert fails; context deadline exceeded during a long transaction.","solutions":["Inspect the wrapped cause (%w) — fix the underlying DB error (disk space, schema, constraint, context cancellation).","Verify the event table schema matches the current version (run bd migrate / upgrade the DB).","Retry the whole unclaim in a fresh transaction; the CAS-based release is safe to re-run.","If the event table is corrupted, restore from backup or rebuild event history before mutating claims."],"exampleFix":"// before — swallowing the wrapped cause\nerr := issueops.UnclaimIssueInTx(ctx, tx, id, actor, false)\nif err != nil { log.Print(err) }\n// after — unwrap and diagnose the event-journal failure\nif err != nil {\n    log.Printf(\"unclaim failed: %v\", errors.Unwrap(err))\n    // e.g. check disk space / run schema migration, then retry\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"err := issueops.UnclaimIssueInTx(ctx, tx, id, actor, force)\nif err != nil && strings.Contains(err.Error(), \"failed to record unclaim event\") {\n    cause := errors.Unwrap(err)\n    log.Printf(\"event journal failure, underlying cause: %v\", cause)\n    // fix DB (disk/schema) and retry the unclaim in a fresh tx\n}","preventionTips":["Keep the event-table schema current (run migrations on upgrade).","Monitor disk space and DB health on the storage backend.","Retry CAS-based unclaims in a fresh transaction after transient DB failures.","Always inspect the wrapped cause (%w) rather than the message string."],"tags":["event-journal","storage","persistence"],"backgroundTag":"event-journal-write-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}