JuliusBrussee/caveman · critical
proposalrun: seq prev_hash does not link to the prior row
Error message
proposalrun: seq %d prev_hash does not link to the prior row
What it means
VerifyChain checks that each row's PrevHash equals the previous row's RowHash (empty for seq 1). This error means the hash link between two adjacent rows is broken: the row's prev_hash was edited, rows were reordered or transplanted from another chain, or a row was inserted without updating descendants' prev_hash values.
Solutions
- Locate the first broken link (the reported seq) and recompute the chain from that point using the library's Append/RowHash functions.
- Restore affected rows from a backup taken before the manual edit.
- Rebuild the whole proposal's chain from the original events if the source of truth is available.
- Prevent direct UPDATE/DELETE on the table (revoke privileges, triggers) so links can only change via the append API.
Example fix
// before
db.Exec("UPDATE proposal_runs SET prev_hash = $1 WHERE seq = $2", someHash, seq)
// after
run := buildNextRun(prevRow, action, detail, cost) // Append computes prev_hash from prevRow.RowHash Defensive patterns
Strategy: try-catch
Validate before calling
func linksIntact(runs []proposalrun.Run) bool {
sorted := append([]proposalrun.Run(nil), runs...)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Seq < sorted[j].Seq })
prev := ""
for _, r := range sorted {
if r.PrevHash != prev { return false }
prev = r.RowHash
}
return true
} Try / catch
if err := proposalrun.VerifyChain(runs); err != nil {
if strings.Contains(err.Error(), "prev_hash does not link") {
// rebuild chain from that seq via the append API
}
return err
} Prevention
- Only write rows through the library's Append helper so prev_hash is chained automatically
- Revoke direct UPDATE on the runs table
- Snapshot/backup the chain regularly for recovery
When it happens
Trigger: Calling VerifyChain(runs) where sorted[i].PrevHash != sorted[i-1].RowHash — e.g. prev_hash manually updated, a row copied from another proposal, or a row appended without chaining to the prior hash.
Common situations: Manual SQL updates to repair 'wrong-looking' hashes, importing rows from a different run set, bugs in custom writers that bypass the Append helper, forked chains after a restore.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- proposalrun: seq out of order (expected )
- proposalrun: seq recompute
- proposalrun: seq row_hash mismatch (tampered)
- cave_harness_incomplete_evidence
- cave_runtime_segment_id_collision
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/4e1df9686206b521.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/proposalrun/proposalrun.go:357
// - each row's row_hash recomputes from its own stored fields (no tampering).
//
// It returns nil for an empty slice (a proposal with no runs yet is consistent).
func VerifyChain(runs []Run) error {
if len(runs) == 0 {
return nil
}
sorted := make([]Run, len(runs))
copy(sorted, runs)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Seq < sorted[j].Seq })
prevHash := ""
for i, r := range sorted {
wantSeq := int64(i + 1)
if r.Seq != wantSeq {
return fmt.Errorf("proposalrun: seq %d out of order (expected %d)", r.Seq, wantSeq)
}
if r.PrevHash != prevHash {
return fmt.Errorf("proposalrun: seq %d prev_hash does not link to the prior row", r.Seq)
}
got, err := RowHash(r.PrevHash, r.Seq, r.Action, r.Detail, r.CostUSD, r.CreatedAt)
if err != nil {
return fmt.Errorf("proposalrun: seq %d recompute: %w", r.Seq, err)
}
if got != r.RowHash {
return fmt.Errorf("proposalrun: seq %d row_hash mismatch (tampered)", r.Seq)
}
prevHash = r.RowHash
}
return nil
}
View on GitHub (pinned to 3ee70a1026)