@@ -5555,123 +5555,137 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
55555555 // is older than the freshness TTL.
55565556 // Best-effort: a failed rebuild leaves the stale-but-working catalog in
55575557 // place and retries on the next read.
5558- const syncStaleConnectionTools = Effect . gen ( function * ( ) {
5559- // The platform view can never persist a rebuilt catalog (writes are
5560- // denied at the storage boundary), so attempting the sync would only
5561- // fire upstream `resolveTools` calls whose results are thrown away —
5562- // network side effects on a read-only credential. Skip it entirely:
5563- // read-only-ness of the platform read path is a stated invariant here,
5564- // not an accident of the best-effort catch below.
5565- if ( config . platformView === true ) return ;
5566- const integrations = yield * core . findMany ( "integration" , { } ) ;
5567- if ( integrations . length === 0 ) return ;
5568- const integrationBySlug = new Map ( integrations . map ( ( row ) => [ row . slug , row ] as const ) ) ;
5569- // The TTL only matters when a loaded plugin actually lists a live remote
5570- // catalog; otherwise skip it so age alone never widens the stale query.
5571- const anyRemoteCatalog = Array . from ( runtimes . values ( ) ) . some (
5572- ( runtime ) => runtime . plugin . remoteToolCatalog === true ,
5573- ) ;
5574- const cutoff =
5575- toolsSyncTtlMs == null || ! anyRemoteCatalog ? null : Date . now ( ) - toolsSyncTtlMs ;
5576-
5577- // Bound the scan to potentially-stale rows: stale-marked (NULL stamp) or
5578- // synced before the latest instant any trigger could fire at (the TTL
5579- // cutoff / the newest config revision). Per-row trigger checks below
5580- // re-verify against each row's own integration; in steady state this
5581- // query returns nothing and the read pays one indexed lookup.
5582- const latestRevision = integrations . reduce < number | null > (
5583- ( max , row ) =>
5584- row . config_revised_at == null
5585- ? max
5586- : Math . max ( max ?? Number ( row . config_revised_at ) , Number ( row . config_revised_at ) ) ,
5587- null ,
5588- ) ;
5589- const staleBefore =
5590- cutoff === null && latestRevision === null
5591- ? null
5592- : Math . max ( cutoff ?? Number . MIN_SAFE_INTEGER , latestRevision ?? Number . MIN_SAFE_INTEGER ) ;
5593-
5594- // A connection with no catalog manifest (built before the manifest
5595- // existed) is stale too: `tools.describeAll` refuses it until a rebuild
5596- // stamps one, so the read that first sees it is the read that fixes it.
5597- const connections = yield * core . findMany ( "connection" , {
5598- where : ( b : AnyCb ) =>
5599- staleBefore === null
5600- ? b . or ( b . isNull ( "tools_synced_at" ) , b . isNull ( "tools_manifest" ) )
5601- : b . or (
5602- b . isNull ( "tools_synced_at" ) ,
5603- b . isNull ( "tools_manifest" ) ,
5604- b ( "tools_synced_at" , "<" , staleBefore ) ,
5605- ) ,
5606- } ) ;
5607- // Each rebuild is an independent upstream listing, so they run together
5608- // rather than one after another: a host with many stale remote-catalog
5609- // connections otherwise pays the sum of every server's latency on the
5610- // read that trips the TTL. Only the listings overlap — `persistCatalog`
5611- // keeps the catalog writes in a single-file queue, so this fan-out never
5612- // opens two transactions on a one-connection database.
5613- const rebuilds : Effect . Effect < readonly Tool [ ] > [ ] = [ ] ;
5614- for ( const connection of connections ) {
5615- const integrationRow = integrationBySlug . get ( connection . integration ) ;
5616- if ( ! integrationRow ) continue ;
5617- const runtime = runtimes . get ( integrationRow . plugin_id ) ;
5618- // Only re-produce catalogs this executor can actually re-list —
5619- // rebuilding under an unloaded plugin would clear a working catalog.
5620- // (A loaded plugin without `resolveTools` still flows through:
5621- // `produceConnectionTools` runs its clear-and-stamp cleanup path.)
5622- if ( ! runtime ) continue ;
5623-
5624- const syncedAt =
5625- connection . tools_synced_at == null ? null : Number ( connection . tools_synced_at ) ;
5626- const revisedTime =
5627- integrationRow . config_revised_at == null
5558+ // `scope` narrows the sync to one integration's connections. A read that
5559+ // only asked for that integration (`describeAll({ integration })`) must not
5560+ // scan, dial, or wait on every other stale connection in the workspace.
5561+ const syncStaleConnectionToolsScoped = ( scope ?: { readonly integration : IntegrationSlug } ) =>
5562+ Effect . gen ( function * ( ) {
5563+ // The platform view can never persist a rebuilt catalog (writes are
5564+ // denied at the storage boundary), so attempting the sync would only
5565+ // fire upstream `resolveTools` calls whose results are thrown away —
5566+ // network side effects on a read-only credential. Skip it entirely:
5567+ // read-only-ness of the platform read path is a stated invariant here,
5568+ // not an accident of the best-effort catch below.
5569+ if ( config . platformView === true ) return ;
5570+ const integrations = yield * core . findMany ( "integration" , {
5571+ where : ( b : AnyCb ) =>
5572+ scope === undefined ? true : b ( "slug" , "=" , String ( scope . integration ) ) ,
5573+ } ) ;
5574+ if ( integrations . length === 0 ) return ;
5575+ const integrationBySlug = new Map ( integrations . map ( ( row ) => [ row . slug , row ] as const ) ) ;
5576+ // The TTL only matters when a loaded plugin actually lists a live remote
5577+ // catalog; otherwise skip it so age alone never widens the stale query.
5578+ const anyRemoteCatalog = Array . from ( runtimes . values ( ) ) . some (
5579+ ( runtime ) => runtime . plugin . remoteToolCatalog === true ,
5580+ ) ;
5581+ const cutoff =
5582+ toolsSyncTtlMs == null || ! anyRemoteCatalog ? null : Date . now ( ) - toolsSyncTtlMs ;
5583+
5584+ // Bound the scan to potentially-stale rows: stale-marked (NULL stamp) or
5585+ // synced before the latest instant any trigger could fire at (the TTL
5586+ // cutoff / the newest config revision). Per-row trigger checks below
5587+ // re-verify against each row's own integration; in steady state this
5588+ // query returns nothing and the read pays one indexed lookup.
5589+ const latestRevision = integrations . reduce < number | null > (
5590+ ( max , row ) =>
5591+ row . config_revised_at == null
5592+ ? max
5593+ : Math . max ( max ?? Number ( row . config_revised_at ) , Number ( row . config_revised_at ) ) ,
5594+ null ,
5595+ ) ;
5596+ const staleBefore =
5597+ cutoff === null && latestRevision === null
56285598 ? null
5629- : Number ( integrationRow . config_revised_at ) ;
5630-
5631- const staleMarked = syncedAt === null || connection . tools_manifest == null ;
5632- const configRevised = revisedTime !== null && ( syncedAt ?? 0 ) < revisedTime ;
5633- const expired =
5634- cutoff !== null &&
5635- runtime . plugin . remoteToolCatalog === true &&
5636- syncedAt !== null &&
5637- syncedAt < cutoff ;
5638- if ( ! staleMarked && ! configRevised && ! expired ) continue ;
5599+ : Math . max (
5600+ cutoff ?? Number . MIN_SAFE_INTEGER ,
5601+ latestRevision ?? Number . MIN_SAFE_INTEGER ,
5602+ ) ;
56395603
5640- rebuilds . push (
5641- produceConnectionTools (
5642- integrationRow ,
5643- {
5644- owner : connection . owner as Owner ,
5645- integration : IntegrationSlug . make ( connection . integration ) ,
5646- name : ConnectionName . make ( connection . name ) ,
5647- } ,
5648- "background" ,
5649- ) . pipe (
5650- // Best-effort, but never silent: the read still succeeds on the
5651- // stale-but-working catalog and the peer rebuilds still finish,
5652- // while the operator gets the connection that failed and why.
5653- // Without this a connection whose upstream is permanently broken
5654- // re-fails on every read and leaves no trace anywhere.
5655- Effect . catch ( ( error ) =>
5656- Effect . logWarning ( "executor stale tool sync failed" , {
5657- integration : connection . integration ,
5658- connection : connection . name ,
5659- error : describeSyncFailure ( error ) ,
5660- } ) . pipe ( Effect . as ( [ ] as readonly Tool [ ] ) ) ,
5604+ // A connection with no catalog manifest (built before the manifest
5605+ // existed) is stale too: `tools.describeAll` refuses it until a rebuild
5606+ // stamps one, so the read that first sees it is the read that fixes it.
5607+ const connections = yield * core . findMany ( "connection" , {
5608+ where : ( b : AnyCb ) =>
5609+ b . and (
5610+ scope === undefined ? true : b ( "integration" , "=" , String ( scope . integration ) ) ,
5611+ staleBefore === null
5612+ ? b . or ( b . isNull ( "tools_synced_at" ) , b . isNull ( "tools_manifest" ) )
5613+ : b . or (
5614+ b . isNull ( "tools_synced_at" ) ,
5615+ b . isNull ( "tools_manifest" ) ,
5616+ b ( "tools_synced_at" , "<" , staleBefore ) ,
5617+ ) ,
56615618 ) ,
5662- Effect . withSpan ( "executor.tools.sync_stale" , {
5663- attributes : {
5664- "executor.integration" : connection . integration ,
5665- "executor.connection" : connection . name ,
5619+ } ) ;
5620+ // Each rebuild is an independent upstream listing, so they run together
5621+ // rather than one after another: a host with many stale remote-catalog
5622+ // connections otherwise pays the sum of every server's latency on the
5623+ // read that trips the TTL. Only the listings overlap — `persistCatalog`
5624+ // keeps the catalog writes in a single-file queue, so this fan-out never
5625+ // opens two transactions on a one-connection database.
5626+ const rebuilds : Effect . Effect < readonly Tool [ ] > [ ] = [ ] ;
5627+ for ( const connection of connections ) {
5628+ const integrationRow = integrationBySlug . get ( connection . integration ) ;
5629+ if ( ! integrationRow ) continue ;
5630+ const runtime = runtimes . get ( integrationRow . plugin_id ) ;
5631+ // Only re-produce catalogs this executor can actually re-list —
5632+ // rebuilding under an unloaded plugin would clear a working catalog.
5633+ // (A loaded plugin without `resolveTools` still flows through:
5634+ // `produceConnectionTools` runs its clear-and-stamp cleanup path.)
5635+ if ( ! runtime ) continue ;
5636+
5637+ const syncedAt =
5638+ connection . tools_synced_at == null ? null : Number ( connection . tools_synced_at ) ;
5639+ const revisedTime =
5640+ integrationRow . config_revised_at == null
5641+ ? null
5642+ : Number ( integrationRow . config_revised_at ) ;
5643+
5644+ const staleMarked = syncedAt === null || connection . tools_manifest == null ;
5645+ const configRevised = revisedTime !== null && ( syncedAt ?? 0 ) < revisedTime ;
5646+ const expired =
5647+ cutoff !== null &&
5648+ runtime . plugin . remoteToolCatalog === true &&
5649+ syncedAt !== null &&
5650+ syncedAt < cutoff ;
5651+ if ( ! staleMarked && ! configRevised && ! expired ) continue ;
5652+
5653+ rebuilds . push (
5654+ produceConnectionTools (
5655+ integrationRow ,
5656+ {
5657+ owner : connection . owner as Owner ,
5658+ integration : IntegrationSlug . make ( connection . integration ) ,
5659+ name : ConnectionName . make ( connection . name ) ,
56665660 } ,
5667- } ) ,
5668- ) ,
5669- ) ;
5670- }
5671- yield * Effect . all ( rebuilds , {
5672- concurrency : STALE_TOOLS_SYNC_CONCURRENCY ,
5661+ "background" ,
5662+ ) . pipe (
5663+ // Best-effort, but never silent: the read still succeeds on the
5664+ // stale-but-working catalog and the peer rebuilds still finish,
5665+ // while the operator gets the connection that failed and why.
5666+ // Without this a connection whose upstream is permanently broken
5667+ // re-fails on every read and leaves no trace anywhere.
5668+ Effect . catch ( ( error ) =>
5669+ Effect . logWarning ( "executor stale tool sync failed" , {
5670+ integration : connection . integration ,
5671+ connection : connection . name ,
5672+ error : describeSyncFailure ( error ) ,
5673+ } ) . pipe ( Effect . as ( [ ] as readonly Tool [ ] ) ) ,
5674+ ) ,
5675+ Effect . withSpan ( "executor.tools.sync_stale" , {
5676+ attributes : {
5677+ "executor.integration" : connection . integration ,
5678+ "executor.connection" : connection . name ,
5679+ } ,
5680+ } ) ,
5681+ ) ,
5682+ ) ;
5683+ }
5684+ yield * Effect . all ( rebuilds , {
5685+ concurrency : STALE_TOOLS_SYNC_CONCURRENCY ,
5686+ } ) ;
56735687 } ) ;
5674- } ) ;
5688+ const syncStaleConnectionTools = syncStaleConnectionToolsScoped ( ) ;
56755689
56765690 // How long a tools read waits for the stale sync before answering from
56775691 // the persisted rows (`ExecutorConfig.toolsSyncGraceMs`; `null` blocks
@@ -5689,10 +5703,13 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
56895703 // failures log and the stale-but-working catalog stays — so the fork
56905704 // swallows its scan errors the same way each rebuild already swallows its
56915705 // own.
5692- const awaitStaleSyncWithinGrace = ( graceMs : number ) =>
5706+ const awaitStaleSyncWithinGrace = (
5707+ graceMs : number ,
5708+ scope ?: { readonly integration : IntegrationSlug } ,
5709+ ) =>
56935710 Effect . gen ( function * ( ) {
56945711 const fiber = yield * Effect . forkDetach (
5695- syncStaleConnectionTools . pipe (
5712+ syncStaleConnectionToolsScoped ( scope ) . pipe (
56965713 Effect . catch ( ( error ) =>
56975714 Effect . logWarning ( "executor stale tool sync scan failed" , {
56985715 error : describeSyncFailure ( error ) ,
@@ -5909,10 +5926,15 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
59095926 filter ?: ToolListFilter ,
59105927 ) : Effect . Effect < readonly ToolProjection [ ] , StorageFailure > =>
59115928 Effect . gen ( function * ( ) {
5929+ // The stale sync is scoped to the requested integration, so a
5930+ // narrowed read never scans, dials, or waits on unrelated
5931+ // connections.
5932+ const scope =
5933+ filter ?. integration === undefined ? undefined : { integration : filter . integration } ;
59125934 if ( toolsSyncGraceMs === null ) {
5913- yield * syncStaleConnectionTools ;
5935+ yield * syncStaleConnectionToolsScoped ( scope ) ;
59145936 } else {
5915- yield * awaitStaleSyncWithinGrace ( toolsSyncGraceMs ) ;
5937+ yield * awaitStaleSyncWithinGrace ( toolsSyncGraceMs , scope ) ;
59165938 }
59175939 const integrationWhere = ( b : AnyCb ) =>
59185940 b . and (
0 commit comments