Skip to content

Commit ad22cf2

Browse files
feat(parser): migrate antigravity providers
Move Antigravity IDE and CLI source discovery, lookup, and parse ownership onto concrete antigravityProvider and antigravityCLIProvider types, deleting the package-level legacy free functions and their legacy sync dispatch. Both agents become provider-authoritative. Sidecar and freshness semantics are preserved through the providers' SourcesForChangedPath fan-out and composite fingerprints rather than engine-level classifiers: the IDE provider maps annotations and brain artifacts back to the conversation DB, and the CLI provider maps history, brain, trajectory, and db/pb-precedence sidecars to every affected source. Drop the obsolete engine-level TestClassifyOnePath_AntigravityCLI, which exercised the removed classifyOnePath antigravity arm. The antigravity provider unit tests cover the per-path sidecar-to-source mappings and the engine integration tests cover the engine-to-provider routing, so the test asserted removed behavior without adding coverage. fix(parser): preserve antigravity history invalidation Antigravity CLI history changes are watched and classified through fresh provider instances, so provider-local history snapshots cannot reliably detect rows that were removed or retagged. Treat history.jsonl writes conservatively and fan out to all current CLI sources, which preserves stale-metadata cleanup at the cost of a broader reparse on history-only updates. The file watcher now consumes provider watch plans for agents that only had plain WatchSubdirs wiring, so provider-owned roots such as Antigravity CLI's history.jsonl parent are observed by the real watcher setup while bespoke legacy watch-root functions keep their existing behavior. Validation: go test -tags fts5 ./internal/parser -count=1; go test -tags fts5 ./internal/sync -run 'Test.*AntigravityCLI|TestProcessAntigravity|TestSyncPathsAntigravity' -count=1; go test -tags fts5 ./cmd/agentsview -run TestCollectWatchRoots -count=1; go vet ./...; git diff --check
1 parent c7c6790 commit ad22cf2

17 files changed

Lines changed: 2674 additions & 910 deletions

cmd/agentsview/main.go

Lines changed: 120 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -874,43 +874,132 @@ func collectWatchRoots(cfg config.Config) (roots []watchRoot, unwatchedDirs []st
874874
continue
875875
}
876876
for _, d := range cfg.ResolveDirs(def.Type) {
877-
if def.ShallowWatchRootsFunc != nil {
878-
for _, watchDir := range def.ShallowWatchRootsFunc(d) {
879-
if _, err := os.Stat(watchDir); err == nil {
880-
addRoot(d, watchDir, true)
881-
}
882-
}
883-
}
884-
if def.WatchRootsFunc != nil {
885-
watchDirs := def.WatchRootsFunc(d)
886-
if len(watchDirs) == 0 {
887-
unwatchedDirs = append(unwatchedDirs, d)
888-
continue
889-
}
890-
for _, watchDir := range watchDirs {
891-
if _, err := os.Stat(watchDir); err == nil {
892-
addRoot(d, watchDir, def.ShallowWatch)
893-
continue
894-
}
895-
unwatchedDirs = append(unwatchedDirs, d)
896-
}
877+
if providerWatched, providerUnwatched := collectProviderWatchRoots(def, d, addRoot); providerWatched {
878+
unwatchedDirs = append(unwatchedDirs, providerUnwatched...)
897879
continue
898880
}
899-
if len(def.WatchSubdirs) == 0 {
900-
if _, err := os.Stat(d); err == nil {
901-
addRoot(d, d, def.ShallowWatch)
902-
}
903-
continue
881+
fallbackUnwatched := collectLegacyWatchRoots(def, d, addRoot)
882+
unwatchedDirs = append(unwatchedDirs, fallbackUnwatched...)
883+
}
884+
}
885+
return roots, unwatchedDirs
886+
}
887+
888+
func collectProviderWatchRoots(
889+
def parser.AgentDef,
890+
dir string,
891+
addRoot func(dir, root string, shallow bool),
892+
) (bool, []string) {
893+
factory, ok := parser.ProviderFactoryByType(def.Type)
894+
if !ok {
895+
return false, nil
896+
}
897+
provider := factory.NewProvider(parser.ProviderConfig{
898+
Roots: []string{dir},
899+
})
900+
plan, err := provider.WatchPlan(context.Background())
901+
if err != nil || len(plan.Roots) == 0 {
902+
if err != nil && !errors.Is(err, parser.ErrUnsupportedProviderFeature) {
903+
log.Printf("%s provider watch plan: %v", def.Type, err)
904+
}
905+
return false, nil
906+
}
907+
added := false
908+
var addedRoots []watchRoot
909+
var missingRoots []string
910+
for _, providerRoot := range plan.Roots {
911+
root := filepath.Clean(providerRoot.Path)
912+
if root == "" || root == "." {
913+
continue
914+
}
915+
if _, err := os.Stat(root); err == nil {
916+
addRoot(dir, root, !providerRoot.Recursive)
917+
added = true
918+
addedRoots = append(addedRoots, watchRoot{
919+
root: root,
920+
shallow: !providerRoot.Recursive,
921+
})
922+
continue
923+
}
924+
missingRoots = append(missingRoots, root)
925+
}
926+
if !added {
927+
return false, nil
928+
}
929+
// A watch target that does not exist yet but lives under an already-watched
930+
// root needs no separate polling only when the ancestor is recursive or
931+
// when a shallow root can observe creation of the missing root itself. A
932+
// shallow ancestor sees only immediate child creation, so it cannot cover a
933+
// missing nested provider root.
934+
for _, missing := range missingRoots {
935+
if !pathCoveredByAnyWatchRootCreation(missing, addedRoots) {
936+
return true, []string{dir}
937+
}
938+
}
939+
return true, nil
940+
}
941+
942+
// pathCoveredByAnyWatchRootCreation reports whether path is covered by an
943+
// existing watch root strongly enough to observe creation of the missing root.
944+
// Recursive roots cover the whole subtree. Shallow roots only cover direct
945+
// children because fsnotify can report that immediate directory creation, after
946+
// which the next watcher setup can add the provider's deeper watch root.
947+
func pathCoveredByAnyWatchRootCreation(path string, roots []watchRoot) bool {
948+
for _, root := range roots {
949+
if root.shallow {
950+
if filepath.Dir(path) == root.root {
951+
return true
904952
}
905-
for _, sub := range def.WatchSubdirs {
906-
watchDir := filepath.Join(d, sub)
907-
if _, err := os.Stat(watchDir); err == nil {
908-
addRoot(d, watchDir, def.ShallowWatch)
909-
}
953+
continue
954+
}
955+
if path == root.root ||
956+
strings.HasPrefix(path, root.root+string(filepath.Separator)) {
957+
return true
958+
}
959+
}
960+
return false
961+
}
962+
963+
func collectLegacyWatchRoots(
964+
def parser.AgentDef,
965+
dir string,
966+
addRoot func(dir, root string, shallow bool),
967+
) []string {
968+
var unwatchedDirs []string
969+
if def.ShallowWatchRootsFunc != nil {
970+
for _, watchDir := range def.ShallowWatchRootsFunc(dir) {
971+
if _, err := os.Stat(watchDir); err == nil {
972+
addRoot(dir, watchDir, true)
910973
}
911974
}
912975
}
913-
return roots, unwatchedDirs
976+
if def.WatchRootsFunc != nil {
977+
watchDirs := def.WatchRootsFunc(dir)
978+
if len(watchDirs) == 0 {
979+
return append(unwatchedDirs, dir)
980+
}
981+
for _, watchDir := range watchDirs {
982+
if _, err := os.Stat(watchDir); err == nil {
983+
addRoot(dir, watchDir, def.ShallowWatch)
984+
continue
985+
}
986+
unwatchedDirs = append(unwatchedDirs, dir)
987+
}
988+
return unwatchedDirs
989+
}
990+
if len(def.WatchSubdirs) == 0 {
991+
if _, err := os.Stat(dir); err == nil {
992+
addRoot(dir, dir, def.ShallowWatch)
993+
}
994+
return unwatchedDirs
995+
}
996+
for _, sub := range def.WatchSubdirs {
997+
watchDir := filepath.Join(dir, sub)
998+
if _, err := os.Stat(watchDir); err == nil {
999+
addRoot(dir, watchDir, def.ShallowWatch)
1000+
}
1001+
}
1002+
return unwatchedDirs
9141003
}
9151004

9161005
func startPeriodicSync(

cmd/agentsview/main_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,31 @@ func TestCollectWatchRootsHermesSessionsWatchesStateDBParent(t *testing.T) {
592592
assert.Equal(t, []string{sessionsDir}, roots[1].dirs)
593593
}
594594

595+
func TestCollectWatchRootsUsesProviderWatchPlan(t *testing.T) {
596+
root := t.TempDir()
597+
for _, dir := range []string{"brain", "conversations", "implicit"} {
598+
require.NoError(t, os.Mkdir(filepath.Join(root, dir), 0o755), "mkdir %s", dir)
599+
}
600+
cfg := config.Config{
601+
AgentDirs: map[parser.AgentType][]string{
602+
parser.AgentAntigravityCLI: {root},
603+
},
604+
}
605+
606+
roots, unwatchedDirs := collectWatchRoots(cfg)
607+
608+
require.Empty(t, unwatchedDirs, "unwatched dirs before watcher setup")
609+
require.Len(t, roots, 4)
610+
assert.Equal(t, filepath.Join(root, "brain"), roots[0].root)
611+
assert.False(t, roots[0].shallow)
612+
assert.Equal(t, filepath.Join(root, "conversations"), roots[1].root)
613+
assert.True(t, roots[1].shallow)
614+
assert.Equal(t, root, roots[2].root)
615+
assert.True(t, roots[2].shallow, "history.jsonl root should be watched shallowly")
616+
assert.Equal(t, filepath.Join(root, "implicit"), roots[3].root)
617+
assert.True(t, roots[3].shallow)
618+
}
619+
595620
func TestResyncCoversSignals(t *testing.T) {
596621
tests := []struct {
597622
name string

internal/parser/antigravity.go

Lines changed: 12 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -30,53 +30,6 @@ var antigravityUUIDLikeRE = regexp.MustCompile(
3030
`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`,
3131
)
3232

33-
// DiscoverAntigravitySessions returns one DiscoveredFile per
34-
// conversations/<uuid>.db under the IDE root.
35-
func DiscoverAntigravitySessions(root string) []DiscoveredFile {
36-
if root == "" {
37-
return nil
38-
}
39-
dir := filepath.Join(root, "conversations")
40-
entries, err := os.ReadDir(dir)
41-
if err != nil {
42-
return nil
43-
}
44-
var files []DiscoveredFile
45-
for _, e := range entries {
46-
if e.IsDir() {
47-
continue
48-
}
49-
name := e.Name()
50-
if !strings.HasSuffix(name, ".db") {
51-
continue
52-
}
53-
id := strings.TrimSuffix(name, ".db")
54-
if !IsValidSessionID(id) {
55-
continue
56-
}
57-
files = append(files, DiscoveredFile{
58-
Path: filepath.Join(dir, name),
59-
Agent: AgentAntigravity,
60-
})
61-
}
62-
sort.Slice(files, func(i, j int) bool {
63-
return files[i].Path < files[j].Path
64-
})
65-
return files
66-
}
67-
68-
// FindAntigravitySourceFile locates a session DB by id.
69-
func FindAntigravitySourceFile(root, id string) string {
70-
if root == "" || !IsValidSessionID(id) {
71-
return ""
72-
}
73-
p := filepath.Join(root, "conversations", id+".db")
74-
if _, err := os.Stat(p); err == nil {
75-
return p
76-
}
77-
return ""
78-
}
79-
8033
// AntigravityFileInfo returns the effective file info for an IDE
8134
// session .db, combining the main file with its -wal/-shm sidecars,
8235
// the annotations/<id>.pbtxt sidecar, and the brain/<id> artifacts
@@ -89,21 +42,29 @@ func AntigravityFileInfo(path string) (os.FileInfo, error) {
8942
if err != nil {
9043
return nil, err
9144
}
45+
return antigravityCLICombinedFileInfo(
46+
info,
47+
antigravityIDECompanionPaths(path)...,
48+
), nil
49+
}
50+
51+
func antigravityIDECompanionPaths(path string) []string {
9252
id := strings.TrimSuffix(filepath.Base(path), ".db")
9353
root := filepath.Dir(filepath.Dir(path))
9454
companions := []string{
9555
path + "-wal",
9656
path + "-shm",
9757
filepath.Join(root, "annotations", id+".pbtxt"),
9858
}
99-
companions = append(companions, antigravityBrainCompanions(
59+
return append(companions, antigravityBrainCompanions(
10060
filepath.Join(root, "brain", id),
10161
)...)
102-
return antigravityCLICombinedFileInfo(info, companions...), nil
10362
}
10463

105-
// ParseAntigravitySession parses one IDE session DB.
106-
func ParseAntigravitySession(
64+
// parseSession parses one IDE session DB. It is owned by the
65+
// antigravityProvider; the package-level ParseAntigravitySession
66+
// entrypoint was folded onto the provider.
67+
func (p *antigravityProvider) parseSession(
10768
path, project, machine string,
10869
) (*ParsedSession, []ParsedMessage, []ParsedUsageEvent, error) {
10970
info, err := os.Stat(path)

0 commit comments

Comments
 (0)