{"record":{"id":"8c258a45415d2a60","repo":"gastownhall/beads","slug":"journal-record-s-for-s-after-seq-counter-heal","errorCode":null,"errorMessage":"journal: record %s for %s after seq counter heal: %w","messagePattern":"journal: record (.+?) for (.+?) after seq counter heal: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/storage/issueops/journal.go","lineNumber":476,"sourceCode":"\t\t// A duplicate seq means the counter is BEHIND the journal — it was\n\t\t// restored, hand-edited, or copied from another workspace. Left alone\n\t\t// that wedges the instance permanently: every later mutation re-mints\n\t\t// the same taken seq and fails, and because the journal row shares the\n\t\t// mutation's transaction, the user's write fails with it. Raise the\n\t\t// counter past the high-water mark and retry exactly once; a second\n\t\t// duplicate is a real bug and must surface, not spin.\n\t\tif !dberrors.IsDuplicateKey(err) {\n\t\t\treturn fmt.Errorf(\"journal: record %s for %s: %w\", op, issueID, err)\n\t\t}\n\t\tif healErr := healEventSeqCounter(ctx, tx); healErr != nil {\n\t\t\treturn healErr\n\t\t}\n\t\tseq, err = nextEventSeq(ctx, tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := insert(seq); err != nil {\n\t\t\treturn fmt.Errorf(\"journal: record %s for %s after seq counter heal: %w\", op, issueID, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// healEventSeqCounter seeds the counter row if it is missing and raises it to\n// the journal's high-water mark, so the next allocation cannot collide. VALUES +\n// GREATEST, not INSERT ... SELECT MAX(): in Dolt a literal+aggregate SELECT over\n// an empty table yields zero rows, so an INSERT ... SELECT would seed nothing on\n// a fresh journal. GREATEST also makes this safe to call on a counter that is\n// already ahead — it never moves the counter backwards.\nfunc healEventSeqCounter(ctx context.Context, tx DBTX) error {\n\tif _, err := tx.ExecContext(ctx, \"INSERT IGNORE INTO bd_events_seq (id, next_seq) VALUES (0, 0)\"); err != nil {\n\t\treturn fmt.Errorf(\"journal: seed seq counter: %w\", err)\n\t}\n\tif _, err := tx.ExecContext(ctx, `\n\t\tUPDATE bd_events_seq\n\t\tSET next_seq = GREATEST(next_seq, COALESCE((SELECT MAX(seq) FROM bd_events_journal), 0))","sourceCodeStart":458,"sourceCodeEnd":494,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/journal.go#L458-L494","documentation":"After a duplicate-key failure, insertEventRow heals the event seq counter and retries the INSERT exactly once; if the retry also fails the error is wrapped as 'journal: record <op> for <id> after seq counter heal'. By design this means the duplicate persisted after the counter was raised past the high-water mark — a real bug (two writers minting the same seq), not a transient condition.","triggerScenarios":"Two transactions concurrently insert journal rows with the same seq even after healing; a stale/incorrect bd_events_seq row that GREATEST-based healing doesn't fix (e.g. journal MAX(seq) read lags an in-flight uncommitted duplicate); manual tampering with bd_events_seq.","commonSituations":"Multiple app instances writing to one shared Dolt database with serialization gaps; operator manually editing bd_events_seq to a low value; driver-level snapshot isolation causing MAX(seq) to miss committed rows.","solutions":["Ensure only one writer (or proper locking) targets the database — the journal shares the caller's transaction","Verify bd_events_seq was not manually modified; let healEventSeqCounter manage it","Compare the retried seq against MAX(seq) in bd_events_journal to confirm the counter state","Report as a bug with logs — a second duplicate is explicitly meant to surface, not spin"],"exampleFix":"// before (operator manual fix, causes repeats)\nUPDATE bd_events_seq SET next_seq = 1 WHERE id = 0;\n// after (let the library heal; never hand-edit)\n-- no manual SQL; retry the failing operation and report if it recurs","handlingStrategy":"fallback","validationCode":"var next, maxSeq int64\n_ = tx.QueryRowContext(ctx, \"SELECT next_seq FROM bd_events_seq WHERE id = 0\").Scan(&next)\n_ = tx.QueryRowContext(ctx, \"SELECT COALESCE(MAX(seq),0) FROM bd_events_journal\").Scan(&maxSeq)\nif next <= maxSeq { return errors.New(\"seq counter behind journal; do not hand-edit, retry write to trigger heal\") }","typeGuard":null,"tryCatchPattern":"if err := doWrite(ctx, tx); err != nil {\n    if strings.Contains(err.Error(), \"after seq counter heal\") {\n        // genuine bug: log full state for report, do NOT retry blindly\n        log.Printf(\"journal seq conflict after heal: %v\", err)\n        return err\n    }\n    return err\n}","preventionTips":["Never hand-edit bd_events_seq","Ensure a single writer or properly serialized transactions on shared databases","Keep all instances on the same beads version","Treat recurrence as a bug report with seq/journal state attached"],"tags":["journal","concurrency","sequence","duplicate-key"],"backgroundTag":"journal-seq-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}