Skip to content

Commit de0417e

Browse files
committed
fix(daemon): scope persistent polling to requesting providers
Persistent obligation scopes were derived from every agent configured on a syncDir, so one provider's persistent-polling fallback reconciled unrelated providers sharing the dir and could tombstone their sessions under a lifecycle-owned missing root. collectWatchRoots now records which provider requested persistent polling for each dir and watchPollingObligations emits persistent scopes only for those requesters, replacing the syncDirToAgents lookup (and the symlinkGatedDirs parameter it existed for).
1 parent d8b7560 commit de0417e

3 files changed

Lines changed: 113 additions & 85 deletions

File tree

cmd/agentsview/main.go

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ func (s watchRecoveryScope) coversProviderRoot(root string) bool {
795795
// probeWatchRecoveryScope computes the probed reconciliation scope backing
796796
// reconcileRootPaths; see that function for the deferral semantics.
797797
func probeWatchRecoveryScope(cfg config.Config) watchRecoveryScope {
798-
roots, unwatchedDirs, symlinkGatedDirs := collectWatchRoots(cfg)
798+
roots, unwatchedDirs, symlinkGatedDirs, _ := collectWatchRoots(cfg)
799799
deferred := make(map[string]struct{})
800800
// A recursive symlink root never joins the watch roots, so its exact
801801
// availability probe is the symlink target itself: os.Stat follows the
@@ -1818,7 +1818,7 @@ func startFileWatcher(
18181818
queueRetry func(sync.WatchBatch),
18191819
) {
18201820
t := time.Now()
1821-
roots, unwatchedDirs, symlinkGatedDirs := collectWatchRoots(cfg)
1821+
roots, unwatchedDirs, symlinkGatedDirs, persistentDirAgents := collectWatchRoots(cfg)
18221822
watcher, err := sync.NewWatcherWithCallback(
18231823
watcherBatchDelay,
18241824
watcherSyncMinInterval,
@@ -1831,7 +1831,7 @@ func startFileWatcher(
18311831
unwatchedDirs = appendUniqueStrings(unwatchedDirs, root.syncDirs()...)
18321832
}
18331833
if coverageErr := registerWatcherUnavailableObligations(
1834-
options, roots, unwatchedDirs, symlinkGatedDirs,
1834+
options, roots, unwatchedDirs, symlinkGatedDirs, persistentDirAgents,
18351835
); coverageErr != nil {
18361836
err = errors.Join(err, coverageErr)
18371837
}
@@ -1878,7 +1878,7 @@ func startFileWatcher(
18781878
}
18791879
}
18801880
if options.OnPollingRequired != nil {
1881-
obligations := watchPollingObligations(roots, results, unwatchedDirs, symlinkGatedDirs)
1881+
obligations := watchPollingObligations(roots, results, unwatchedDirs, persistentDirAgents)
18821882
obligations = append(obligations, symlinkPollingObligations(symlinkGatedDirs)...)
18831883
for _, obligation := range obligations {
18841884
if err := options.OnPollingRequired(obligation); err != nil {
@@ -1911,11 +1911,18 @@ func startFileWatcher(
19111911
watcher.QueueRetryBatch
19121912
}
19131913

1914+
// watchPollingObligations builds the polling obligations for the watch plan.
1915+
// persistentDirAgents maps each clean persistent-polling dir to the providers
1916+
// that requested persistent polling for it (from collectWatchRoots); persistent
1917+
// obligations carry scopes only for those requesters. Other agents that merely
1918+
// share the configured dir are covered by their own watch results, and pulling
1919+
// them into another provider's persistent poll would reconcile them
1920+
// authoritatively — tombstoning sessions under a lifecycle-owned missing root.
19141921
func watchPollingObligations(
19151922
roots []watchRoot,
19161923
results []sync.RecursiveWatchResult,
19171924
unwatchedDirs []string,
1918-
symlinkGatedDirs map[string][]watchScope,
1925+
persistentDirAgents map[string][]parser.AgentType,
19191926
) []sync.PollingObligation {
19201927
type draft struct {
19211928
probe string
@@ -1944,32 +1951,6 @@ func watchPollingObligations(
19441951
}
19451952
}
19461953

1947-
// syncDirToAgents maps clean syncDir → all agents configured for that dir.
1948-
// Include both regular roots and symlink-gated dirs so that a provider
1949-
// whose only physical root is a symlink (excluded from the watch plan) still
1950-
// has its agent recorded for persistent obligation scopes.
1951-
syncDirToAgents := make(map[string][]parser.AgentType)
1952-
for _, root := range roots {
1953-
for _, scope := range root.scopes {
1954-
if scope.syncDir != "" {
1955-
cleanDir := filepath.Clean(scope.syncDir)
1956-
if !slices.Contains(syncDirToAgents[cleanDir], scope.agent) {
1957-
syncDirToAgents[cleanDir] = append(syncDirToAgents[cleanDir], scope.agent)
1958-
}
1959-
}
1960-
}
1961-
}
1962-
for _, scopes := range symlinkGatedDirs {
1963-
for _, scope := range scopes {
1964-
if scope.syncDir != "" {
1965-
cleanDir := filepath.Clean(scope.syncDir)
1966-
if !slices.Contains(syncDirToAgents[cleanDir], scope.agent) {
1967-
syncDirToAgents[cleanDir] = append(syncDirToAgents[cleanDir], scope.agent)
1968-
}
1969-
}
1970-
}
1971-
}
1972-
19731954
for i, root := range roots {
19741955
var result sync.RecursiveWatchResult
19751956
if i < len(results) {
@@ -1981,7 +1962,7 @@ func watchPollingObligations(
19811962
}
19821963
for _, dir := range root.persistentPollingDirs {
19831964
cleanDir := filepath.Clean(dir)
1984-
agents := syncDirToAgents[cleanDir]
1965+
agents := persistentDirAgents[cleanDir]
19851966
if len(agents) == 0 {
19861967
addScope(pollingObligationKey("persistent", cleanDir), dir,
19871968
pollingScope{Root: dir})
@@ -2012,7 +1993,7 @@ func watchPollingObligations(
20121993
for _, dir := range unwatchedDirs {
20131994
cleanDir := filepath.Clean(dir)
20141995
if _, ok := represented[cleanDir]; !ok {
2015-
agents := syncDirToAgents[cleanDir]
1996+
agents := persistentDirAgents[cleanDir]
20161997
if len(agents) == 0 {
20171998
addScope(pollingObligationKey("persistent", cleanDir), cleanDir,
20181999
pollingScope{Root: dir})
@@ -2073,8 +2054,9 @@ func registerWatcherUnavailableObligations(
20732054
roots []watchRoot,
20742055
unwatchedDirs []string,
20752056
symlinkGatedDirs map[string][]watchScope,
2057+
persistentDirAgents map[string][]parser.AgentType,
20762058
) error {
2077-
obligations := watchPollingObligations(roots, nil, unwatchedDirs, symlinkGatedDirs)
2059+
obligations := watchPollingObligations(roots, nil, unwatchedDirs, persistentDirAgents)
20782060
obligations = append(obligations, symlinkPollingObligations(symlinkGatedDirs)...)
20792061
if options.OnPollingRequired != nil {
20802062
for _, obligation := range obligations {
@@ -2503,14 +2485,27 @@ func (r watchRoot) pollingScopesForDirs(dirs []string) []pollingScope {
25032485
// each recursive provider root skipped because it is a symlink to the
25042486
// configured dirs whose reconciliation scope its target availability gates;
25052487
// those roots never join the watcher plan or the returned roots.
2488+
// persistentDirAgents maps each clean persistent-polling dir to the providers
2489+
// that requested persistent polling for it, so obligation scopes can stay
2490+
// limited to the owning providers rather than every agent sharing the dir.
25062491
func collectWatchRoots(cfg config.Config) (
25072492
roots []watchRoot,
25082493
unwatchedDirs []string,
25092494
symlinkGatedDirs map[string][]watchScope,
2495+
persistentDirAgents map[string][]parser.AgentType,
25102496
) {
25112497
rootIndexes := make(map[string]int)
25122498
persistentPollingDirs := make(map[string]struct{})
25132499
symlinkGatedDirs = make(map[string][]watchScope)
2500+
persistentDirAgents = make(map[string][]parser.AgentType)
2501+
addPersistent := func(agent parser.AgentType, dir string) {
2502+
persistentPollingDirs[dir] = struct{}{}
2503+
unwatchedDirs = appendUniqueString(unwatchedDirs, dir)
2504+
cleanDir := filepath.Clean(dir)
2505+
if !slices.Contains(persistentDirAgents[cleanDir], agent) {
2506+
persistentDirAgents[cleanDir] = append(persistentDirAgents[cleanDir], agent)
2507+
}
2508+
}
25142509
addRoot := func(agent parser.AgentType, dir, path string, recursive, exists bool) {
25152510
path = filepath.Clean(path)
25162511
scope := watchScope{agent: agent, syncDir: dir}
@@ -2538,8 +2533,7 @@ func collectWatchRoots(cfg config.Config) (
25382533
_, hasProvider := parser.ProviderFactoryByType(def.Type)
25392534
if providerWatched, polling := collectProviderWatchRoots(def, d, addAgentRoot); providerWatched {
25402535
if polling.persistent {
2541-
persistentPollingDirs[d] = struct{}{}
2542-
unwatchedDirs = appendUniqueString(unwatchedDirs, d)
2536+
addPersistent(def.Type, d)
25432537
}
25442538
for _, symRoot := range polling.symlinkRoots {
25452539
scope := watchScope{agent: def.Type, syncDir: d}
@@ -2561,15 +2555,13 @@ func collectWatchRoots(cfg config.Config) (
25612555
}
25622556
if !def.FileBased {
25632557
if hasProvider {
2564-
persistentPollingDirs[d] = struct{}{}
2565-
unwatchedDirs = appendUniqueString(unwatchedDirs, d)
2558+
addPersistent(def.Type, d)
25662559
}
25672560
continue
25682561
}
25692562
fallbackUnwatched := collectLegacyWatchRoots(def, d, addAgentRoot)
25702563
for _, pollingDir := range fallbackUnwatched {
2571-
persistentPollingDirs[pollingDir] = struct{}{}
2572-
unwatchedDirs = appendUniqueString(unwatchedDirs, pollingDir)
2564+
addPersistent(def.Type, pollingDir)
25732565
}
25742566
}
25752567
}
@@ -2583,7 +2575,7 @@ func collectWatchRoots(cfg config.Config) (
25832575
}
25842576
}
25852577
}
2586-
return roots, unwatchedDirs, symlinkGatedDirs
2578+
return roots, unwatchedDirs, symlinkGatedDirs, persistentDirAgents
25872579
}
25882580

25892581
type providerPollingReasons struct {
@@ -2917,7 +2909,7 @@ type scheduledReconcileTarget struct {
29172909
// present scope would read the missing one as an authoritative empty discovery
29182910
// and tombstone every session beneath it.
29192911
func scheduledReconcileTargets(cfg config.Config) []scheduledReconcileTarget {
2920-
roots, _, _ := collectWatchRoots(cfg)
2912+
roots, _, _, _ := collectWatchRoots(cfg)
29212913
deferred := make(map[parser.AgentType]map[string]struct{})
29222914
for _, root := range roots {
29232915
if root.exists {

cmd/agentsview/main_test.go

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ func TestCollectWatchRootsPreservesDirsSharingWatchRoot(t *testing.T) {
561561
},
562562
}
563563

564-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
564+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
565565

566566
assert.ElementsMatch(t, []string{sessionsDir, archivedDir}, unwatchedDirs,
567567
"missing roots retain polling until native activation completes")
@@ -610,7 +610,7 @@ func TestCollectWatchRootsPollsRecursiveSymlinkProviderRoot(t *testing.T) {
610610
},
611611
}
612612

613-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
613+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
614614

615615
require.Len(t, roots, 2)
616616
assert.Equal(t, root, roots[0].path)
@@ -1086,7 +1086,7 @@ func TestCollectWatchRootsHermesSessionsWatchesStateDBParent(t *testing.T) {
10861086
},
10871087
}
10881088

1089-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
1089+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
10901090

10911091
require.Empty(t, unwatchedDirs, "unwatched dirs before watcher setup")
10921092
require.Len(t, roots, 2)
@@ -1109,7 +1109,7 @@ func TestCollectWatchRootsWatchesHermesProfilesContainerRecursively(t *testing.T
11091109
},
11101110
}
11111111

1112-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
1112+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
11131113

11141114
require.Empty(t, unwatchedDirs)
11151115
require.Len(t, roots, 1)
@@ -1127,7 +1127,7 @@ func TestCollectWatchRootsUsesCoworkProviderRecursiveRoot(t *testing.T) {
11271127
},
11281128
}
11291129

1130-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
1130+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
11311131

11321132
require.Empty(t, unwatchedDirs, "cowork root should be watched directly")
11331133
got, ok := findCollectedWatchRoot(roots, root)
@@ -1148,7 +1148,7 @@ func TestCollectWatchRootsUsesGeminiProviderMetadataRoot(t *testing.T) {
11481148
},
11491149
}
11501150

1151-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
1151+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
11521152

11531153
require.Empty(t, unwatchedDirs, "all gemini provider roots exist")
11541154
metadataRoot, ok := findCollectedWatchRoot(roots, root)
@@ -1172,7 +1172,7 @@ func TestCollectWatchRootsUsesAntigravityCLIHistoryRoot(t *testing.T) {
11721172
},
11731173
}
11741174

1175-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
1175+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
11761176

11771177
require.Empty(t, unwatchedDirs, "all antigravity cli provider roots exist")
11781178
historyRoot, ok := findCollectedWatchRoot(roots, root)
@@ -1199,7 +1199,7 @@ func TestCollectWatchRootsIncludesDevinProviderRootsForNonFileAgent(t *testing.T
11991199
},
12001200
}
12011201

1202-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
1202+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
12031203

12041204
require.Empty(t, unwatchedDirs)
12051205
cliRoot, ok := findCollectedWatchRoot(roots, filepath.Join(root, "cli"))
@@ -1221,7 +1221,7 @@ func TestCollectWatchRootsTracksExactAgentsForSharedRoot(t *testing.T) {
12211221
parser.AgentCodex: {root},
12221222
}}
12231223

1224-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
1224+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
12251225

12261226
require.Empty(t, unwatchedDirs)
12271227
shared, ok := findCollectedWatchRoot(roots, root)
@@ -1247,7 +1247,7 @@ func TestCollectWatchRootsPreservesMissingProviderRoots(t *testing.T) {
12471247
},
12481248
}
12491249

1250-
roots, unwatchedDirs, _ := collectWatchRoots(cfg)
1250+
roots, unwatchedDirs, _, _ := collectWatchRoots(cfg)
12511251

12521252
require.Len(t, roots, 2)
12531253
cliRoot, ok := findCollectedWatchRoot(roots, filepath.Join(root, "cli"))
@@ -1538,7 +1538,7 @@ func TestOpenCodeFormatMissingRootsUseNativeLifecycleWithoutPolling(t *testing.T
15381538
parser.AgentMiMoCode: dirs,
15391539
}}
15401540

1541-
roots, unwatched, _ := collectWatchRoots(cfg)
1541+
roots, unwatched, _, persistentDirAgents := collectWatchRoots(cfg)
15421542
require.Len(t, roots, rootCount)
15431543
results := make([]agentsync.RecursiveWatchResult, rootCount)
15441544
for i := range results {
@@ -1549,7 +1549,7 @@ func TestOpenCodeFormatMissingRootsUseNativeLifecycleWithoutPolling(t *testing.T
15491549
}
15501550
unwatched = accountRegisteredWatchRoots(unwatched, roots, results)
15511551

1552-
assert.Empty(t, watchPollingObligations(roots, results, unwatched, nil),
1552+
assert.Empty(t, watchPollingObligations(roots, results, unwatched, persistentDirAgents),
15531553
"absent OpenCode-format providers must not add archive-scale polling")
15541554
})
15551555
}
@@ -1575,7 +1575,7 @@ func TestWatchPollingObligationsKeepPendingAndPersistentReasonsIndependent(t *te
15751575
roots,
15761576
[]agentsync.RecursiveWatchResult{{Watched: 1}, {Watched: 1}},
15771577
[]string{shared},
1578-
nil,
1578+
map[string][]parser.AgentType{shared: {parser.AgentDevin}},
15791579
)
15801580

15811581
assert.Equal(t, []agentsync.PollingObligation{
@@ -1695,6 +1695,7 @@ func TestWatcherUnavailableFallbackDefersBrokenSymlinkScope(t *testing.T) {
16951695
nil,
16961696
[]string{parent, other},
16971697
map[string][]watchScope{symRoot: {{syncDir: parent}}},
1698+
nil,
16981699
))
16991700

17001701
bothDirs := []string{parent, other}
@@ -1774,7 +1775,7 @@ func TestWatcherUnavailableFallbackDefersMissingNestedRootScope(t *testing.T) {
17741775
}}
17751776

17761777
require.NoError(t, registerWatcherUnavailableObligations(
1777-
options, roots, []string{parent, other}, nil,
1778+
options, roots, []string{parent, other}, nil, nil,
17781779
))
17791780

17801781
coordinator.requestPoll()
@@ -1859,7 +1860,7 @@ func TestWatcherUnavailableFallbackDefersNestedRootLostAfterRegistration(t *test
18591860
}}
18601861

18611862
require.NoError(t, registerWatcherUnavailableObligations(
1862-
options, roots, []string{parent, other}, nil,
1863+
options, roots, []string{parent, other}, nil, nil,
18631864
))
18641865

18651866
// Per-agent polling: parent (gemini) and other ("") arrive as separate calls.
@@ -1941,7 +1942,7 @@ func TestRegisterWatcherUnavailableCoversDegradedWithoutPollingRequired(t *testi
19411942
}}
19421943

19431944
require.NoError(t, registerWatcherUnavailableObligations(
1944-
options, roots, []string{parent, other}, nil,
1945+
options, roots, []string{parent, other}, nil, nil,
19451946
))
19461947

19471948
// Pre-fix, probeGated excludes parent (its scope has a probe=nestedRoot
@@ -2002,7 +2003,7 @@ func TestWatcherUnavailableObligationsGateBeforeFallback(t *testing.T) {
20022003
}}
20032004

20042005
require.NoError(t, registerWatcherUnavailableObligations(
2005-
options, roots, []string{parent, other}, nil,
2006+
options, roots, []string{parent, other}, nil, nil,
20062007
))
20072008

20082009
require.NotEmpty(t, stepAvailable)

0 commit comments

Comments
 (0)