From 9c72317b9b497671ebe0de8e38993f638e5fa056 Mon Sep 17 00:00:00 2001 From: xiaoen <2768753269@qq.com> Date: Thu, 26 Feb 2026 16:13:57 +0800 Subject: [PATCH] fix(memory): write meta before JSONL rewrite for crash safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- pkg/memory/jsonl.go | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/pkg/memory/jsonl.go b/pkg/memory/jsonl.go index 6e6722b96..222d91f02 100644 --- a/pkg/memory/jsonl.go +++ b/pkg/memory/jsonl.go @@ -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.