fix(memory): write meta before JSONL rewrite for crash safety

In SetHistory and Compact, the JSONL file was rewritten before updating
the meta file. If the process crashed between the two writes, the meta
still had a large Skip value pointing past the now-shorter JSONL file,
causing GetHistory to return empty — effectively data loss.

Reverse the order: write meta (with Skip=0) first, then rewrite JSONL.
On crash between the two writes, the old uncompacted file is still
intact and GetHistory reads from line 1, returning stale-but-complete
data. The next operation self-corrects.
This commit is contained in:
xiaoen
2026-02-26 16:13:57 +08:00
parent 1f0b85280a
commit 9c72317b9b
+21 -12
View File
@@ -361,11 +361,6 @@ func (s *JSONLStore) SetHistory(
l.Lock()
defer l.Unlock()
err := s.rewriteJSONL(sessionKey, history)
if err != nil {
return err
}
meta, err := s.readMeta(sessionKey)
if err != nil {
return err
@@ -378,7 +373,16 @@ func (s *JSONLStore) SetHistory(
meta.Count = len(history)
meta.UpdatedAt = now
return s.writeMeta(sessionKey, meta)
// Write meta BEFORE rewriting the JSONL file. If we crash between
// the two writes, meta has Skip=0 and the old file is still intact,
// so GetHistory reads from line 1 — returning "too many" messages
// rather than losing data. The next SetHistory call corrects this.
err = s.writeMeta(sessionKey, meta)
if err != nil {
return err
}
return s.rewriteJSONL(sessionKey, history)
}
// Compact physically rewrites the JSONL file, dropping all logically
@@ -409,16 +413,21 @@ func (s *JSONLStore) Compact(
return err
}
err = s.rewriteJSONL(sessionKey, active)
if err != nil {
return err
}
// Write meta BEFORE rewriting the JSONL file. If the process
// crashes between the two writes, meta has Skip=0 and the old
// (uncompacted) file is still intact, so GetHistory reads from
// line 1 — returning previously-truncated messages rather than
// losing data. The next Compact or TruncateHistory corrects this.
meta.Skip = 0
meta.Count = len(active)
meta.UpdatedAt = time.Now()
return s.writeMeta(sessionKey, meta)
err = s.writeMeta(sessionKey, meta)
if err != nil {
return err
}
return s.rewriteJSONL(sessionKey, active)
}
// rewriteJSONL atomically replaces the JSONL file with the given messages.