Skip to content

Commit 034548c

Browse files
fix(parser): define sqlite provider deletion semantics
Shared SQLite providers need to treat deletion events as source-level state, not as unclassifiable paths. Without that, a deleted Zed or Shelley database can disappear before the provider facade reports a complete empty source, leaving future cleanup behavior under-specified. Classifying syntactically valid DB, WAL, and SHM paths even after the main DB is gone preserves the watcher-to-parse path. A missing backing DB now produces a complete force-replace SkipNoSession outcome, and the Zed fingerprint comment records the intentional legacy whole-DB hash tradeoff.
1 parent 7ada88b commit 034548c

3 files changed

Lines changed: 73 additions & 2 deletions

File tree

internal/parser/shelley_provider.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ func (p *shelleyProvider) Parse(
8787
machine := firstNonEmptyJSONLString(req.Machine, p.Config.Machine)
8888
dbInfo, err := os.Stat(src.DBPath)
8989
if err != nil {
90+
if os.IsNotExist(err) {
91+
return ParseOutcome{
92+
ResultSetComplete: true,
93+
ForceReplace: true,
94+
SkipReason: SkipNoSession,
95+
}, nil
96+
}
9097
return ParseOutcome{}, fmt.Errorf("stat %s: %w", src.DBPath, err)
9198
}
9299

@@ -359,7 +366,7 @@ func (s shelleySourceSet) sourceRefForChangedPath(
359366
return s.newSourceRef(root, path, dbPath, conversationID), true
360367
}
361368
dbPath, ok := shelleyDBPathForEvent(root, path)
362-
if !ok || !IsRegularFile(dbPath) {
369+
if !ok {
363370
return SourceRef{}, false
364371
}
365372
return s.newSourceRef(root, dbPath, dbPath, ""), true

internal/parser/zed_provider.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ func (p *zedProvider) Parse(
8787
machine := firstNonEmptyJSONLString(req.Machine, p.Config.Machine)
8888
dbInfo, err := os.Stat(src.DBPath)
8989
if err != nil {
90+
if os.IsNotExist(err) {
91+
return ParseOutcome{
92+
ResultSetComplete: true,
93+
ForceReplace: true,
94+
SkipReason: SkipNoSession,
95+
}, nil
96+
}
9097
return ParseOutcome{}, fmt.Errorf("stat %s: %w", src.DBPath, err)
9198
}
9299

@@ -286,6 +293,9 @@ func (s zedSourceSet) Fingerprint(
286293
} else if compositeMtime, err := sqliteDBCompositeMtime(src.DBPath); err == nil {
287294
mtime = compositeMtime
288295
}
296+
// Zed has no cheap per-thread content digest; legacy sync stored the
297+
// physical DB hash on virtual thread rows while per-thread updated_at
298+
// remained the mtime freshness signal.
289299
hash, err := hashJSONLSourceFile(src.DBPath)
290300
if err != nil {
291301
return SourceFingerprint{}, err
@@ -346,7 +356,7 @@ func (s zedSourceSet) sourceRefForChangedPath(root, path string) (SourceRef, boo
346356
return s.newSourceRef(root, path, dbPath, sessionID), true
347357
}
348358
dbPath, ok := zedDBPathForEvent(root, path)
349-
if !ok || !IsRegularFile(dbPath) {
359+
if !ok {
350360
return SourceRef{}, false
351361
}
352362
return s.newSourceRef(root, dbPath, dbPath, ""), true

internal/parser/zed_shelley_provider_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,37 @@ func TestZedProviderFingerprintIncludesWALSiblings(t *testing.T) {
177177
assert.Greater(t, after.MTimeNS, before.MTimeNS)
178178
}
179179

180+
func TestZedProviderClassifiesDeletedPhysicalDB(t *testing.T) {
181+
root := t.TempDir()
182+
dbPath := filepath.Join(root, zedThreadsDBRelPath)
183+
require.NoError(t, os.MkdirAll(filepath.Dir(dbPath), 0o755))
184+
createZedThreadsDBAt(t, dbPath, []zedTestThread{{
185+
id: "10431c84-c47b-4e6c-b2df-f9f3b9ad025b",
186+
summary: "Provider thread",
187+
updatedAt: "2026-06-08T09:14:10Z",
188+
dataType: "json",
189+
data: []byte(`{"messages":[{"User":{"content":[{"Text":"Hello Zed"}]}}]}`),
190+
}})
191+
require.NoError(t, os.Remove(dbPath))
192+
193+
provider, ok := NewProvider(AgentZed, ProviderConfig{Roots: []string{root}})
194+
require.True(t, ok)
195+
changed, err := provider.SourcesForChangedPath(
196+
context.Background(),
197+
ChangedPathRequest{Path: dbPath, EventKind: "remove", WatchRoot: filepath.Dir(dbPath)},
198+
)
199+
require.NoError(t, err)
200+
require.Len(t, changed, 1)
201+
assert.Equal(t, dbPath, changed[0].DisplayPath)
202+
203+
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: changed[0]})
204+
require.NoError(t, err)
205+
assert.True(t, outcome.ResultSetComplete)
206+
assert.True(t, outcome.ForceReplace)
207+
assert.Equal(t, SkipNoSession, outcome.SkipReason)
208+
assert.Empty(t, outcome.Results)
209+
}
210+
180211
func TestShelleyProviderFactoryReplacesLegacyAdapter(t *testing.T) {
181212
factory, ok := ProviderFactoryByType(AgentShelley)
182213
require.True(t, ok)
@@ -359,3 +390,26 @@ func TestShelleyProviderClassifiesDeletedVirtualPath(t *testing.T) {
359390
require.Len(t, changed, 1)
360391
assert.Equal(t, virtualPath, changed[0].DisplayPath)
361392
}
393+
394+
func TestShelleyProviderClassifiesDeletedPhysicalDB(t *testing.T) {
395+
root, dbPath, db := newShelleyTestDB(t)
396+
seedShelleyMainConversation(t, db)
397+
require.NoError(t, os.Remove(dbPath))
398+
399+
provider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{root}})
400+
require.True(t, ok)
401+
changed, err := provider.SourcesForChangedPath(
402+
context.Background(),
403+
ChangedPathRequest{Path: dbPath, EventKind: "remove", WatchRoot: root},
404+
)
405+
require.NoError(t, err)
406+
require.Len(t, changed, 1)
407+
assert.Equal(t, dbPath, changed[0].DisplayPath)
408+
409+
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: changed[0]})
410+
require.NoError(t, err)
411+
assert.True(t, outcome.ResultSetComplete)
412+
assert.True(t, outcome.ForceReplace)
413+
assert.Equal(t, SkipNoSession, outcome.SkipReason)
414+
assert.Empty(t, outcome.Results)
415+
}

0 commit comments

Comments
 (0)