@@ -123,7 +123,7 @@ import {
123123 hasWorkspaceAdminAccess ,
124124} from '@/lib/workspaces/permissions/utils'
125125import { computeNeedsRedeployment } from '@/app/api/workflows/utils'
126- import { buildCustomBlockConfig } from '@/blocks/custom/build-config'
126+ import { buildCustomBlockConfig , isCustomBlockType } from '@/blocks/custom/build-config'
127127import { getAllBlocks } from '@/blocks/registry'
128128import type { BlockIcon } from '@/blocks/types'
129129import { CONNECTOR_REGISTRY } from '@/connectors/registry.server'
@@ -405,6 +405,18 @@ export class WorkspaceVFS {
405405 private deploymentCache = new Map < string , Promise < DeploymentData | null > > ( )
406406 private _workspaceId = ''
407407 private _betaEnabled = false
408+ /**
409+ * Types of the org's CURRENT custom blocks (enabled + disabled — a disabled block
410+ * still resolves/renders). Populated by {@link materializeCustomBlocks}; used to
411+ * drop a placed custom block from a workflow's state when its definition has been
412+ * deleted, so the copilot never sees a block it can't render.
413+ *
414+ * `null` means "not loaded" — either not materialized yet or the load FAILED. In
415+ * that case {@link dropDeletedCustomBlocks} strips nothing, so a transient failure
416+ * can't wrongly nuke every placed custom block. An empty `Set` is distinct: it
417+ * means the org genuinely has no custom blocks, so any placed one IS deleted.
418+ */
419+ private _customBlockTypes : Set < string > | null = null
408420
409421 get workspaceId ( ) : string {
410422 return this . _workspaceId
@@ -425,12 +437,45 @@ export class WorkspaceVFS {
425437 ) : Promise < Awaited < ReturnType < typeof loadWorkflowFromNormalizedTables > > > {
426438 let cached = this . normalizedCache . get ( workflowId )
427439 if ( ! cached ) {
428- cached = loadWorkflowFromNormalizedTables ( workflowId )
440+ cached = loadWorkflowFromNormalizedTables ( workflowId ) . then ( ( n ) =>
441+ this . dropDeletedCustomBlocks ( n )
442+ )
429443 this . normalizedCache . set ( workflowId , cached )
430444 }
431445 return cached
432446 }
433447
448+ /**
449+ * Strip placed custom blocks whose definition no longer exists from a loaded
450+ * workflow (and any edges touching them), so the copilot never sees a block it
451+ * can't render — mirroring how the serializer drops an unresolvable custom block.
452+ * A live definition (enabled or disabled) is kept; only a DELETED one is removed.
453+ * Runs lazily (after materialize), so `_customBlockTypes` is populated by then.
454+ */
455+ private dropDeletedCustomBlocks (
456+ normalized : Awaited < ReturnType < typeof loadWorkflowFromNormalizedTables > >
457+ ) : Awaited < ReturnType < typeof loadWorkflowFromNormalizedTables > > {
458+ // `null` = definitions never loaded (or the load failed) — strip nothing rather
459+ // than treat every placed custom block as deleted.
460+ if ( ! normalized || this . _customBlockTypes === null ) return normalized
461+ const validTypes = this . _customBlockTypes
462+ const dropped = new Set < string > ( )
463+ const blocks : Record < string , unknown > = { }
464+ for ( const [ id , block ] of Object . entries ( normalized . blocks ) ) {
465+ const type = ( block as { type ?: string } ) . type
466+ if ( isCustomBlockType ( type ) && ! validTypes . has ( type ) ) {
467+ dropped . add ( id )
468+ continue
469+ }
470+ blocks [ id ] = block
471+ }
472+ if ( dropped . size === 0 ) return normalized
473+ const edges = ( normalized . edges ?? [ ] ) . filter (
474+ ( e ) => ! dropped . has ( e . source ) && ! dropped . has ( e . target )
475+ )
476+ return { ...normalized , blocks : blocks as typeof normalized . blocks , edges }
477+ }
478+
434479 /** Load a workflow's deployment data once per instance (deployment.json + versions.json share it). */
435480 private loadDeployments ( wf : {
436481 id : string
@@ -523,6 +568,7 @@ export class WorkspaceVFS {
523568 this . lazy = new Map ( )
524569 this . normalizedCache = new Map ( )
525570 this . deploymentCache = new Map ( )
571+ this . _customBlockTypes = null
526572 this . _workspaceId = workspaceId
527573 this . _betaEnabled = await isFeatureEnabled ( 'mothership-beta' , { userId } )
528574
@@ -1824,6 +1870,9 @@ export class WorkspaceVFS {
18241870 ) : Promise < NonNullable < WorkspaceMdData [ 'customBlocks' ] > > {
18251871 try {
18261872 const blocks = await listCustomBlocksWithInputsForWorkspace ( workspaceId )
1873+ // Every current definition (incl. disabled) — the authoritative set used to
1874+ // drop deleted-definition instances from workflow state (see loadNormalized).
1875+ this . _customBlockTypes = new Set ( blocks . map ( ( cb ) => cb . type ) )
18271876 const summary : NonNullable < WorkspaceMdData [ 'customBlocks' ] > = [ ]
18281877
18291878 for ( const cb of blocks ) {
0 commit comments