@@ -134,15 +134,13 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | HttpClie
134134 Effect . map ( ( v ) => v as Record < string , Provider > | undefined ) ,
135135 )
136136
137- // Bundled snapshot fallback — present in built releases, missing in dev .
137+ // Bundled at build time; absent in dev — `tryPromise` covers both .
138138 const loadSnapshot = Effect . tryPromise ( {
139139 // @ts -ignore — generated at build time, may not exist in dev
140140 try : ( ) => import ( "./models-snapshot.js" ) . then ( ( m ) => m . snapshot as Record < string , Provider > | undefined ) ,
141141 catch : ( ) => undefined ,
142142 } ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( undefined ) ) )
143143
144- // Cross-process file lock — Flock is the right primitive (in-process
145- // semaphores can't coordinate concurrent opencode CLIs writing the same cache).
146144 const fetchAndWrite = Effect . fn ( "ModelsDev.fetchAndWrite" ) ( function * ( ) {
147145 const text = yield * fetchApi ( )
148146 yield * fs . writeWithDirs ( filepath , text )
@@ -155,41 +153,40 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | HttpClie
155153 const snapshot = yield * loadSnapshot
156154 if ( snapshot ) return snapshot
157155 if ( Flag . OPENCODE_DISABLE_MODELS_FETCH ) return { }
158- return yield * Effect . scoped (
156+ // Flock is cross-process: concurrent opencode CLIs can race on this cache file.
157+ const text = yield * Effect . scoped (
159158 Effect . gen ( function * ( ) {
160159 yield * Flock . effect ( lockKey )
161- const text = yield * fetchAndWrite ( )
162- return JSON . parse ( text ) as Record < string , Provider >
160+ return yield * fetchAndWrite ( )
163161 } ) ,
164162 )
163+ return JSON . parse ( text ) as Record < string , Provider >
165164 } ) . pipe ( Effect . withSpan ( "ModelsDev.populate" ) , Effect . orDie )
166165
167- // Single-flight cache: concurrent first-call dedupes via the cached effect.
168- // refresh() invalidates so the next get() re-populates from disk.
169166 const [ cachedGet , invalidate ] = yield * Effect . cachedInvalidateWithTTL ( populate , Duration . infinity )
170167
171168 const get = ( ) : Effect . Effect < Record < string , Provider > > => cachedGet
172169
173170 const refresh = Effect . fn ( "ModelsDev.refresh" ) ( function * ( force = false ) {
174- if ( ! force && ( yield * fresh ( ) ) ) return yield * invalidate
171+ if ( ! force && ( yield * fresh ( ) ) ) return
175172 yield * Effect . scoped (
176173 Effect . gen ( function * ( ) {
177174 yield * Flock . effect ( lockKey )
175+ // Re-check under the lock: another process may have refreshed between
176+ // our outer check and lock acquisition.
178177 if ( ! force && ( yield * fresh ( ) ) ) return
179178 yield * fetchAndWrite ( )
179+ yield * invalidate
180180 } ) ,
181181 ) . pipe (
182182 Effect . tapCause ( ( cause ) => Effect . logError ( "Failed to fetch models.dev" , { cause } ) ) ,
183183 Effect . ignore ,
184184 )
185- yield * invalidate
186185 } )
187186
188187 if ( ! Flag . OPENCODE_DISABLE_MODELS_FETCH && ! process . argv . includes ( "--get-yargs-completions" ) ) {
189- // Eager initial refresh + 1 hour cadence (matches pre-Service behavior).
190- yield * Effect . forkScoped (
191- refresh ( ) . pipe ( Effect . andThen ( refresh ( ) . pipe ( Effect . repeat ( Schedule . fixed ( "60 minutes" ) ) ) ) , Effect . ignore ) ,
192- )
188+ // Schedule.spaced runs the effect once, then waits between completions.
189+ yield * Effect . forkScoped ( refresh ( ) . pipe ( Effect . repeat ( Schedule . spaced ( "60 minutes" ) ) , Effect . ignore ) )
193190 }
194191
195192 return Service . of ( { get, refresh } )
0 commit comments