@@ -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.
797797func 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.
19141921func 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.
25062491func 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
25892581type 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.
29192911func 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 {
0 commit comments