Skip to content

Commit 384821f

Browse files
fix(sync): apply hermes archive fingerprints consistently
Hermes archive refresh paths need to compare and persist the same aggregate fingerprint for state.db plus sibling transcripts. Otherwise cached parse skips and single-session refreshes can fall back to raw state.db metadata and miss transcript-only archive changes. Use the aggregate archive file info before generic skip-cache checks and share the archive parse-and-stamp helper between full archive processing and single-session refreshes. The regression coverage now persists the metadata, checks unchanged archive skips, and covers transcript discovery/removal behavior. Validation: go test -tags "fts5" ./internal/sync -run 'TestHermesArchive|TestProcessFileHermes|TestProcessHermesArchive|TestSyncSingleHermesArchive' -count=1; go test -tags "fts5" ./internal/parser ./internal/sync; go vet ./...; make nilaway
1 parent 963a393 commit 384821f

2 files changed

Lines changed: 172 additions & 15 deletions

File tree

internal/sync/engine.go

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4316,6 +4316,9 @@ func (e *Engine) processFile(
43164316
err: fmt.Errorf("stat %s: %w", file.Path, err),
43174317
}
43184318
}
4319+
if file.Agent == parser.AgentHermes {
4320+
info = hermesArchiveEffectiveInfo(file.Path, info)
4321+
}
43194322

43204323
// Capture mtime once from the initial stat so all
43214324
// downstream cache operations use a consistent value.
@@ -6385,25 +6388,16 @@ func (e *Engine) processCortex(
63856388
func (e *Engine) processHermes(
63866389
file parser.DiscoveredFile, info os.FileInfo,
63876390
) processResult {
6388-
if filepath.Base(file.Path) == "state.db" {
6389-
info = hermesArchiveEffectiveInfo(file.Path, info)
6390-
}
6391+
info = hermesArchiveEffectiveInfo(file.Path, info)
63916392
if e.shouldSkipByPath(file.Path, info) {
63926393
return processResult{skip: true}
63936394
}
63946395

63956396
if filepath.Base(file.Path) == "state.db" {
6396-
results, err := parser.ParseHermesArchive(
6397-
file.Path, file.Project, e.machine,
6398-
)
6397+
results, err := e.parseHermesArchive(file.Path, file.Project, info)
63996398
if err != nil {
64006399
return processResult{err: err}
64016400
}
6402-
for i := range results {
6403-
results[i].Session.File.Path = file.Path
6404-
results[i].Session.File.Size = info.Size()
6405-
results[i].Session.File.Mtime = info.ModTime().UnixNano()
6406-
}
64076401
return processResult{results: results, forceReplace: true}
64086402
}
64096403

@@ -6429,7 +6423,37 @@ func (e *Engine) processHermes(
64296423
}
64306424
}
64316425

6426+
func (e *Engine) parseHermesArchive(
6427+
stateDB, project string, info os.FileInfo,
6428+
) ([]parser.ParseResult, error) {
6429+
info = hermesArchiveEffectiveInfo(stateDB, info)
6430+
results, err := parser.ParseHermesArchive(
6431+
stateDB, project, e.machine,
6432+
)
6433+
if err != nil {
6434+
return nil, err
6435+
}
6436+
stampHermesArchiveResults(stateDB, info, results)
6437+
return results, nil
6438+
}
6439+
6440+
func stampHermesArchiveResults(
6441+
stateDB string, info os.FileInfo, results []parser.ParseResult,
6442+
) {
6443+
for i := range results {
6444+
results[i].Session.File.Path = stateDB
6445+
results[i].Session.File.Size = info.Size()
6446+
results[i].Session.File.Mtime = info.ModTime().UnixNano()
6447+
}
6448+
}
6449+
64326450
func hermesArchiveEffectiveInfo(path string, info os.FileInfo) os.FileInfo {
6451+
if info == nil {
6452+
return info
6453+
}
6454+
if _, ok := info.(fakeSnapshotInfo); ok {
6455+
return info
6456+
}
64336457
_, sessionsDir, ok := hermesArchiveSourcePaths(path)
64346458
if !ok {
64356459
return info
@@ -9269,9 +9293,11 @@ func (e *Engine) syncSingleHermesArchive(
92699293
return false, nil
92709294
}
92719295

9272-
results, err := parser.ParseHermesArchive(
9273-
stateDB, project, e.machine,
9274-
)
9296+
info, err := os.Stat(stateDB)
9297+
if err != nil {
9298+
return true, fmt.Errorf("stat %s: %w", stateDB, err)
9299+
}
9300+
results, err := e.parseHermesArchive(stateDB, project, info)
92759301
if err != nil {
92769302
return true, err
92779303
}

internal/sync/hermes_archive_test.go

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sync
22

33
import (
4+
"context"
45
"database/sql"
56
"os"
67
"path/filepath"
@@ -35,6 +36,76 @@ func TestHermesArchiveEffectiveInfoIncludesDirectTranscripts(t *testing.T) {
3536
assert.Equal(t, transcriptTime.UnixNano(), got.ModTime().UnixNano())
3637
}
3738

39+
func TestHermesArchiveTranscriptFilesUsesDirectSessionFiles(t *testing.T) {
40+
sessionsDir := t.TempDir()
41+
writeFile := func(name string) {
42+
t.Helper()
43+
path := filepath.Join(sessionsDir, name)
44+
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
45+
require.NoError(t, os.WriteFile(path, []byte("{}\n"), 0o644))
46+
}
47+
writeFile("extra.jsonl")
48+
writeFile("session_child.json")
49+
writeFile("child.json")
50+
writeFile("session_child.txt")
51+
writeFile(filepath.Join("nested", "session_nested.json"))
52+
53+
got := hermesArchiveTranscriptFiles(sessionsDir)
54+
55+
assert.Equal(t, []string{
56+
filepath.Join(sessionsDir, "extra.jsonl"),
57+
filepath.Join(sessionsDir, "session_child.json"),
58+
}, got)
59+
}
60+
61+
func TestHermesArchiveEffectiveInfoChangesWhenTranscriptRemoved(t *testing.T) {
62+
root := t.TempDir()
63+
stateDB := writeHermesArchiveStateDB(t, root)
64+
transcriptPath := filepath.Join(root, "sessions", "extra.jsonl")
65+
require.NoError(t, os.MkdirAll(filepath.Dir(transcriptPath), 0o755))
66+
require.NoError(t, os.WriteFile(transcriptPath, []byte("{}\n{}\n"), 0o644))
67+
68+
stateInfo, err := os.Stat(stateDB)
69+
require.NoError(t, err)
70+
before := hermesArchiveEffectiveInfo(stateDB, stateInfo)
71+
72+
require.NoError(t, os.Remove(transcriptPath))
73+
after := hermesArchiveEffectiveInfo(stateDB, stateInfo)
74+
75+
assert.NotEqual(t, before.Size(), after.Size())
76+
assert.Equal(t, stateInfo.Size(), after.Size())
77+
}
78+
79+
func TestProcessFileHermesArchiveSkipCacheUsesAggregateMtime(t *testing.T) {
80+
root := t.TempDir()
81+
stateDB := writeHermesArchiveStateDB(t, root)
82+
transcriptPath := filepath.Join(root, "sessions", "extra.jsonl")
83+
require.NoError(t, os.MkdirAll(filepath.Dir(transcriptPath), 0o755))
84+
require.NoError(t, os.WriteFile(transcriptPath, []byte("{}\n"), 0o644))
85+
transcriptTime := time.Now().Add(2 * time.Second).Truncate(time.Second)
86+
require.NoError(t, os.Chtimes(transcriptPath, transcriptTime, transcriptTime))
87+
88+
engine := NewEngine(dbtest.OpenTestDB(t), EngineConfig{
89+
AgentDirs: map[parser.AgentType][]string{
90+
parser.AgentHermes: {filepath.Join(root, "sessions")},
91+
},
92+
Machine: "local",
93+
})
94+
engine.InjectSkipCache(map[string]int64{
95+
stateDB: transcriptTime.UnixNano(),
96+
})
97+
98+
res := engine.processFile(context.Background(), parser.DiscoveredFile{
99+
Path: stateDB,
100+
Agent: parser.AgentHermes,
101+
})
102+
103+
require.NoError(t, res.err)
104+
assert.True(t, res.skip)
105+
assert.True(t, res.cacheSkip)
106+
assert.Equal(t, transcriptTime.UnixNano(), res.mtime)
107+
}
108+
38109
func TestProcessHermesArchivePersistsAggregateFingerprint(t *testing.T) {
39110
root := t.TempDir()
40111
stateDB := writeHermesArchiveStateDB(t, root)
@@ -52,7 +123,8 @@ func TestProcessHermesArchivePersistsAggregateFingerprint(t *testing.T) {
52123
stateInfo, err := os.Stat(stateDB)
53124
require.NoError(t, err)
54125
effectiveInfo := hermesArchiveEffectiveInfo(stateDB, stateInfo)
55-
engine := NewEngine(dbtest.OpenTestDB(t), EngineConfig{
126+
database := dbtest.OpenTestDB(t)
127+
engine := NewEngine(database, EngineConfig{
56128
AgentDirs: map[parser.AgentType][]string{
57129
parser.AgentHermes: {filepath.Join(root, "sessions")},
58130
},
@@ -71,6 +143,65 @@ func TestProcessHermesArchivePersistsAggregateFingerprint(t *testing.T) {
71143
assert.Equal(t, effectiveInfo.Size(), result.Session.File.Size)
72144
assert.Equal(t, effectiveInfo.ModTime().UnixNano(), result.Session.File.Mtime)
73145
}
146+
147+
pending := make([]pendingWrite, 0, len(res.results))
148+
for _, result := range res.results {
149+
pending = append(pending, pendingWrite{
150+
sess: result.Session,
151+
msgs: result.Messages,
152+
usageEvents: result.UsageEvents,
153+
})
154+
}
155+
written, _, failed := engine.writeBatch(pending, syncWriteDefault, true)
156+
require.Equal(t, 0, failed)
157+
require.NotZero(t, written)
158+
159+
storedSize, storedMtime, ok := database.GetFileInfoByPath(stateDB)
160+
require.True(t, ok)
161+
assert.Equal(t, effectiveInfo.Size(), storedSize)
162+
assert.Equal(t, effectiveInfo.ModTime().UnixNano(), storedMtime)
163+
164+
second := engine.processHermes(parser.DiscoveredFile{
165+
Path: stateDB,
166+
Agent: parser.AgentHermes,
167+
}, stateInfo)
168+
require.NoError(t, second.err)
169+
assert.True(t, second.skip)
170+
}
171+
172+
func TestSyncSingleHermesArchivePersistsAggregateFingerprint(t *testing.T) {
173+
root := t.TempDir()
174+
stateDB := writeHermesArchiveStateDB(t, root)
175+
transcriptPath := filepath.Join(root, "sessions", "extra.jsonl")
176+
require.NoError(t, os.MkdirAll(filepath.Dir(transcriptPath), 0o755))
177+
require.NoError(t, os.WriteFile(
178+
transcriptPath,
179+
[]byte(
180+
`{"role":"session_meta","platform":"cli","timestamp":"2026-05-14T10:00:00.000000"}`+"\n"+
181+
`{"role":"user","content":"new transcript","timestamp":"2026-05-14T10:01:00.000000"}`+"\n",
182+
),
183+
0o644,
184+
))
185+
186+
stateInfo, err := os.Stat(stateDB)
187+
require.NoError(t, err)
188+
effectiveInfo := hermesArchiveEffectiveInfo(stateDB, stateInfo)
189+
database := dbtest.OpenTestDB(t)
190+
engine := NewEngine(database, EngineConfig{
191+
AgentDirs: map[parser.AgentType][]string{
192+
parser.AgentHermes: {filepath.Join(root, "sessions")},
193+
},
194+
Machine: "local",
195+
})
196+
197+
ok, err := engine.syncSingleHermesArchive("hermes:child", transcriptPath, "")
198+
require.NoError(t, err)
199+
require.True(t, ok)
200+
201+
storedSize, storedMtime, found := database.GetFileInfoByPath(stateDB)
202+
require.True(t, found)
203+
assert.Equal(t, effectiveInfo.Size(), storedSize)
204+
assert.Equal(t, effectiveInfo.ModTime().UnixNano(), storedMtime)
74205
}
75206

76207
func writeHermesArchiveStateDB(t *testing.T, root string) string {

0 commit comments

Comments
 (0)