Skip to content

Commit ede515d

Browse files
fix(parser): preserve gptme provider source parity
The gptme provider is intended to be a no-behavior-change facade migration, so it needs to preserve the legacy source semantics before sync callers can safely move to it. Symlinked session directories, deleted source events, and persisted lookup hints are all observable through the current discovery and session lookup paths. This keeps provider-backed gptme discovery and changed-path classification compatible with those legacy expectations while leaving runtime dispatch unchanged.
1 parent 9814bf5 commit ede515d

2 files changed

Lines changed: 190 additions & 13 deletions

File tree

internal/parser/gptme_provider.go

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,15 @@ func (p *gptmeProvider) SourcesForChangedPath(
6161
if err != nil {
6262
return nil, err
6363
}
64-
return p.filterSources(sources), nil
64+
filtered := p.filterSources(sources)
65+
if len(filtered) > 0 {
66+
return filtered, nil
67+
}
68+
source, ok := p.sourceForEventPath(req)
69+
if !ok {
70+
return nil, nil
71+
}
72+
return []SourceRef{source}, nil
6573
}
6674

6775
func (p *gptmeProvider) FindSource(
@@ -71,25 +79,88 @@ func (p *gptmeProvider) FindSource(
7179
if err := ctx.Err(); err != nil {
7280
return SourceRef{}, false, err
7381
}
74-
if req.StoredFilePath != "" {
75-
source, ok := p.sources.sourceForPath(req.StoredFilePath)
76-
if ok && p.isSource(source) {
82+
for _, path := range []string{
83+
req.StoredFilePath,
84+
req.FingerprintKey,
85+
} {
86+
if path == "" {
87+
continue
88+
}
89+
if source, ok := p.sourceForExistingPath(path); ok {
7790
return source, true, nil
7891
}
7992
}
80-
if req.RawSessionID == "" {
81-
return SourceRef{}, false, nil
82-
}
83-
for _, root := range p.Config.Roots {
84-
path := filepath.Join(root, req.RawSessionID, "conversation.jsonl")
85-
source, ok := p.sources.sourceForPath(path)
86-
if ok && p.isSource(source) {
93+
for _, id := range []string{
94+
req.RawSessionID,
95+
p.rawSessionIDFromFull(req.FullSessionID),
96+
} {
97+
if id == "" {
98+
continue
99+
}
100+
if source, ok := p.sourceForSessionID(id); ok {
87101
return source, true, nil
88102
}
89103
}
90104
return SourceRef{}, false, nil
91105
}
92106

107+
func (p *gptmeProvider) sourceForExistingPath(path string) (SourceRef, bool) {
108+
source, ok := p.sources.sourceForPath(path)
109+
if ok && p.isSource(source) {
110+
return source, true
111+
}
112+
return SourceRef{}, false
113+
}
114+
115+
func (p *gptmeProvider) sourceForSessionID(id string) (SourceRef, bool) {
116+
for _, root := range p.Config.Roots {
117+
path := filepath.Join(root, id, "conversation.jsonl")
118+
if source, ok := p.sourceForExistingPath(path); ok {
119+
return source, true
120+
}
121+
}
122+
return SourceRef{}, false
123+
}
124+
125+
func (p *gptmeProvider) rawSessionIDFromFull(id string) string {
126+
if id == "" {
127+
return ""
128+
}
129+
_, rawID := StripHostPrefix(id)
130+
if !strings.HasPrefix(rawID, p.Def.IDPrefix) {
131+
return ""
132+
}
133+
return strings.TrimPrefix(rawID, p.Def.IDPrefix)
134+
}
135+
136+
func (p *gptmeProvider) sourceForEventPath(req ChangedPathRequest) (SourceRef, bool) {
137+
if req.Path == "" {
138+
return SourceRef{}, false
139+
}
140+
if req.WatchRoot != "" {
141+
root := filepath.Clean(req.WatchRoot)
142+
if !p.hasRoot(root) {
143+
return SourceRef{}, false
144+
}
145+
return gptmeSourceRef(root, filepath.Clean(req.Path))
146+
}
147+
for _, root := range p.Config.Roots {
148+
if source, ok := gptmeSourceRef(root, filepath.Clean(req.Path)); ok {
149+
return source, true
150+
}
151+
}
152+
return SourceRef{}, false
153+
}
154+
155+
func (p *gptmeProvider) hasRoot(root string) bool {
156+
for _, configured := range p.Config.Roots {
157+
if samePath(configured, root) {
158+
return true
159+
}
160+
}
161+
return false
162+
}
163+
93164
func (p *gptmeProvider) Fingerprint(
94165
ctx context.Context,
95166
source SourceRef,
@@ -157,8 +228,9 @@ func (p *gptmeProvider) isSource(source SourceRef) bool {
157228

158229
func newGptmeSourceSet(roots []string) JSONLSourceSet {
159230
return NewJSONLSourceSet(AgentGptme, roots, JSONLSourceSetOptions{
160-
Recursive: true,
161-
Hash: true,
231+
Recursive: true,
232+
Hash: true,
233+
FollowSymlinkDirs: true,
162234
Include: func(path string, info os.FileInfo) bool {
163235
return !info.IsDir() && filepath.Base(path) == "conversation.jsonl"
164236
},
@@ -210,3 +282,24 @@ func gptmeSessionIDFromPath(root, path string) string {
210282
}
211283
return filepath.Base(filepath.Dir(path))
212284
}
285+
286+
func gptmeSourceRef(root, path string) (SourceRef, bool) {
287+
root = filepath.Clean(root)
288+
path = filepath.Clean(path)
289+
if !isGptmeConversationPath(root, path) {
290+
return SourceRef{}, false
291+
}
292+
sessionID := gptmeSessionIDFromPath(root, path)
293+
return SourceRef{
294+
Provider: AgentGptme,
295+
Key: path,
296+
DisplayPath: path,
297+
FingerprintKey: path,
298+
ProjectHint: gptmeProjectFromSessionName(sessionID),
299+
Opaque: JSONLSource{
300+
Root: root,
301+
Path: path,
302+
RelPath: filepath.Join(sessionID, "conversation.jsonl"),
303+
},
304+
}, true
305+
}

internal/parser/gptme_provider_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package parser
33
import (
44
"context"
55
"errors"
6+
"os"
67
"path/filepath"
78
"testing"
89

@@ -82,6 +83,89 @@ func TestGptmeProviderSourceMethods(t *testing.T) {
8283
assert.NotEmpty(t, fingerprint.Hash)
8384
}
8485

86+
func TestGptmeProviderDiscoversSymlinkSessionDirectories(t *testing.T) {
87+
root := t.TempDir()
88+
targetRoot := t.TempDir()
89+
sessionID := "2026-06-13-write-hello-world"
90+
targetDir := filepath.Join(targetRoot, sessionID)
91+
writeSourceFile(
92+
t,
93+
filepath.Join(targetDir, "conversation.jsonl"),
94+
gptmeProviderFixture(),
95+
)
96+
linkDir := filepath.Join(root, sessionID)
97+
if err := os.Symlink(targetDir, linkDir); err != nil {
98+
t.Skipf("creating directory symlink: %v", err)
99+
}
100+
101+
provider, ok := NewProvider(AgentGptme, ProviderConfig{
102+
Roots: []string{root},
103+
Machine: "devbox",
104+
})
105+
require.True(t, ok)
106+
107+
discovered, err := provider.Discover(context.Background())
108+
require.NoError(t, err)
109+
require.Len(t, discovered, 1)
110+
assert.Equal(t, filepath.Join(linkDir, "conversation.jsonl"), discovered[0].DisplayPath)
111+
112+
legacy := DiscoverGptmeSessions(root)
113+
require.Len(t, legacy, 1)
114+
assert.Equal(t, legacy[0].Path, discovered[0].DisplayPath)
115+
}
116+
117+
func TestGptmeProviderClassifiesDeletedConversationPath(t *testing.T) {
118+
root := t.TempDir()
119+
sessionID := "2026-06-13-write-hello-world"
120+
sourcePath := filepath.Join(root, sessionID, "conversation.jsonl")
121+
writeSourceFile(t, sourcePath, gptmeProviderFixture())
122+
123+
provider, ok := NewProvider(AgentGptme, ProviderConfig{
124+
Roots: []string{root},
125+
Machine: "devbox",
126+
})
127+
require.True(t, ok)
128+
require.NoError(t, os.Remove(sourcePath))
129+
130+
changed, err := provider.SourcesForChangedPath(
131+
context.Background(),
132+
ChangedPathRequest{
133+
Path: sourcePath,
134+
EventKind: "remove",
135+
WatchRoot: root,
136+
},
137+
)
138+
require.NoError(t, err)
139+
require.Len(t, changed, 1)
140+
assert.Equal(t, sourcePath, changed[0].Key)
141+
assert.Equal(t, sourcePath, changed[0].DisplayPath)
142+
assert.Equal(t, "write-hello-world", changed[0].ProjectHint)
143+
}
144+
145+
func TestGptmeProviderFindSourceUsesPersistedFallbacks(t *testing.T) {
146+
root := t.TempDir()
147+
sessionID := "2026-06-13-write-hello-world"
148+
sourcePath := filepath.Join(root, sessionID, "conversation.jsonl")
149+
writeSourceFile(t, sourcePath, gptmeProviderFixture())
150+
151+
provider, ok := NewProvider(AgentGptme, ProviderConfig{
152+
Roots: []string{root},
153+
Machine: "devbox",
154+
})
155+
require.True(t, ok)
156+
157+
for _, req := range []FindSourceRequest{
158+
{FingerprintKey: sourcePath},
159+
{FullSessionID: "gptme:" + sessionID},
160+
{FullSessionID: "host~gptme:" + sessionID},
161+
} {
162+
found, ok, err := provider.FindSource(context.Background(), req)
163+
require.NoError(t, err)
164+
require.Truef(t, ok, "request %#v", req)
165+
assert.Equal(t, sourcePath, found.DisplayPath)
166+
}
167+
}
168+
85169
func TestGptmeProviderParse(t *testing.T) {
86170
root := t.TempDir()
87171
sessionID := "2026-06-13-write-hello-world"

0 commit comments

Comments
 (0)