From 19d775c43578696baa1db1e0eaf93e701752fa2a Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Sun, 8 Mar 2026 10:24:44 +0100 Subject: [PATCH 01/10] Merge commit from fork --- .../backend/backend/api/features/chat/routes.py | 1 - autogpt_platform/backend/backend/copilot/service.py | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/autogpt_platform/backend/backend/api/features/chat/routes.py b/autogpt_platform/backend/backend/api/features/chat/routes.py index 4564e7323499..2c2265b5e941 100644 --- a/autogpt_platform/backend/backend/api/features/chat/routes.py +++ b/autogpt_platform/backend/backend/api/features/chat/routes.py @@ -753,7 +753,6 @@ async def event_generator() -> AsyncGenerator[str, None]: @router.patch( "/sessions/{session_id}/assign-user", dependencies=[Security(auth.requires_user)], - status_code=200, ) async def session_assign_user( session_id: str, diff --git a/autogpt_platform/backend/backend/copilot/service.py b/autogpt_platform/backend/backend/copilot/service.py index 2bcba33abdd6..d2d61f1438e1 100644 --- a/autogpt_platform/backend/backend/copilot/service.py +++ b/autogpt_platform/backend/backend/copilot/service.py @@ -18,7 +18,7 @@ from backend.data.db_accessors import understanding_db from backend.data.understanding import format_understanding_for_prompt -from backend.util.exceptions import NotFoundError +from backend.util.exceptions import NotAuthorizedError, NotFoundError from backend.util.settings import AppEnvironment, Settings from .config import ChatConfig @@ -298,6 +298,12 @@ async def assign_user_to_session( session = await get_chat_session(session_id, None) if not session: raise NotFoundError(f"Session {session_id} not found") + if session.user_id is not None and session.user_id != user_id: + logger.warning( + f"[SECURITY] Attempt to claim session {session_id} by user {user_id}, " + f"but it already belongs to user {session.user_id}" + ) + raise NotAuthorizedError(f"Not authorized to claim session {session_id}") session.user_id = user_id session = await upsert_chat_session(session) return session From b57c9d78d828e02c581464c8cfc805d658aa3c7a Mon Sep 17 00:00:00 2001 From: eureka928 Date: Mon, 9 Mar 2026 11:56:35 +0100 Subject: [PATCH 02/10] fix(frontend/builder): batch undo history for cascading operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When deleting a node, React Flow fires onNodesChange and onEdgesChange as separate callbacks, each pushing to the undo history. This created intermediate states that broke undo — requiring multiple undos and restoring inconsistent graph states (e.g. edges pointing to deleted nodes). Use microtask-based batching in pushState so all calls within the same synchronous execution are coalesced into a single history entry, keeping only the first pre-change snapshot. Fixes #10999 --- .../(platform)/build/stores/historyStore.ts | 63 ++++++++++++++----- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/build/stores/historyStore.ts b/autogpt_platform/frontend/src/app/(platform)/build/stores/historyStore.ts index 3a67bb8dcdb4..5f1b2ee7744e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/stores/historyStore.ts +++ b/autogpt_platform/frontend/src/app/(platform)/build/stores/historyStore.ts @@ -25,34 +25,60 @@ type HistoryStore = { const MAX_HISTORY = 50; +// Microtask batching state — kept outside the store to avoid triggering +// re-renders. When multiple pushState calls happen in the same synchronous +// execution (e.g. node deletion cascading to edge cleanup), only the first +// (pre-change) state is kept and committed as a single history entry. +let pendingState: HistoryState | null = null; +let batchScheduled = false; + export const useHistoryStore = create((set, get) => ({ past: [{ nodes: [], edges: [] }], future: [], pushState: (state: HistoryState) => { - const { past } = get(); - const lastState = past[past.length - 1]; - - if (lastState && isEqual(lastState, state)) { - return; + // Keep only the first state within a microtask batch — it represents + // the true pre-change snapshot before any cascading mutations. + if (!pendingState) { + pendingState = state; } - const actualCurrentState = { - nodes: useNodeStore.getState().nodes, - edges: useEdgeStore.getState().edges, - }; + if (!batchScheduled) { + batchScheduled = true; + queueMicrotask(() => { + const stateToCommit = pendingState; + pendingState = null; + batchScheduled = false; - if (isEqual(state, actualCurrentState)) { - return; - } + if (!stateToCommit) return; - set((prev) => ({ - past: [...prev.past.slice(-MAX_HISTORY + 1), state], - future: [], - })); + const { past } = get(); + const lastState = past[past.length - 1]; + + if (lastState && isEqual(lastState, stateToCommit)) { + return; + } + + const actualCurrentState = { + nodes: useNodeStore.getState().nodes, + edges: useEdgeStore.getState().edges, + }; + + if (isEqual(stateToCommit, actualCurrentState)) { + return; + } + + set((prev) => ({ + past: [...prev.past.slice(-MAX_HISTORY + 1), stateToCommit], + future: [], + })); + }); + } }, initializeHistory: () => { + pendingState = null; + const currentNodes = useNodeStore.getState().nodes; const currentEdges = useEdgeStore.getState().edges; @@ -122,5 +148,8 @@ export const useHistoryStore = create((set, get) => ({ }, canRedo: () => get().future.length > 0, - clear: () => set({ past: [{ nodes: [], edges: [] }], future: [] }), + clear: () => { + pendingState = null; + set({ past: [{ nodes: [], edges: [] }], future: [] }); + }, })); From e50b751da6d77f963de0a40e362f4b3a8772434b Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Tue, 10 Mar 2026 11:21:22 +0530 Subject: [PATCH 03/10] Add OneOf field support with discriminator-based rendering --- .../nodes/CustomNode/CustomNode.tsx | 12 +- .../components/NodeAdvancedToggle.tsx | 19 +- .../renderers/InputRenderer/FormRenderer.tsx | 5 +- .../InputRenderer/base/anyof/useAnyOfField.ts | 1 - .../InputRenderer/base/base-registry.ts | 2 + .../InputRenderer/base/oneof/OneOfField.tsx | 219 ++++++++++++++++++ .../base/standard/FieldTemplate.tsx | 14 +- .../base/standard/TitleField.tsx | 4 +- .../InputRenderer/utils/schema-utils.ts | 8 + 9 files changed, 269 insertions(+), 15 deletions(-) create mode 100644 autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx index 62e796b74815..a9b33d7f604c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/CustomNode/CustomNode.tsx @@ -23,6 +23,12 @@ import { WebhookDisclaimer } from "./components/WebhookDisclaimer"; import { SubAgentUpdateFeature } from "./components/SubAgentUpdate/SubAgentUpdateFeature"; import { useCustomNode } from "./useCustomNode"; +function hasAdvancedFields(schema: RJSFSchema): boolean { + const properties = schema?.properties; + if (!properties) return false; + return Object.values(properties).some((prop: any) => prop.advanced === true); +} + export type CustomNodeData = { hardcodedValues: { [key: string]: any; @@ -108,7 +114,11 @@ export const CustomNode: React.FC> = React.memo( )} showHandles={showHandles} /> - + {data.uiType != BlockUIType.OUTPUT && ( state.nodeAdvancedStates[nodeId] || false, ); const setShowAdvanced = useNodeStore((state) => state.setShowAdvanced); + + if (!hasAdvancedFields) return null; + return ( -
+
+ )} {fieldSchema?.description && ( @@ -102,13 +124,10 @@ export const OutputHandler = ({ )}
- {/* Recursively render nested properties */} - {fieldSchema?.properties && - renderOutputHandles( - fieldSchema.properties, - fullKey, - `${fieldTitle}.`, - )} + {/* Nested properties: collapsed by default */} + {hasNestedProperties && + isExpanded && + renderOutputHandles(fieldSchema.properties!, fullKey)}
) : null; }, From 098622d5ec4f2130520ec4348c6b74ac0e69061c Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Tue, 10 Mar 2026 11:59:16 +0530 Subject: [PATCH 05/10] Improve output visibility filtering in FlowEditor Filter nested outputs to show only connected/broken fields when collapsed, while still displaying all children when explicitly expanded by the user. --- .../FlowEditor/nodes/OutputHandler.tsx | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/OutputHandler.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/OutputHandler.tsx index b674b4969c0a..ed071ef0b9f7 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/OutputHandler.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/OutputHandler.tsx @@ -40,10 +40,28 @@ export const OutputHandler = ({ setExpandedObjects((prev) => ({ ...prev, [key]: !prev[key] })); } + function hasConnectedOrBrokenDescendant( + schema: RJSFSchema, + keyPrefix: string, + ): boolean { + if (!schema) return false; + return Object.entries(schema).some( + ([key, fieldSchema]: [string, RJSFSchema]) => { + const fullKey = keyPrefix ? `${keyPrefix}_#_${key}` : key; + if (isOutputConnected(nodeId, fullKey) || brokenOutputs.has(fullKey)) + return true; + if (fieldSchema?.properties) + return hasConnectedOrBrokenDescendant(fieldSchema.properties, fullKey); + return false; + }, + ); + } + const renderOutputHandles = ( schema: RJSFSchema, keyPrefix: string = "", titlePrefix: string = "", + connectedOnly: boolean = false, ): React.ReactNode[] => { return Object.entries(schema).map( ([key, fieldSchema]: [string, RJSFSchema]) => { @@ -51,13 +69,24 @@ export const OutputHandler = ({ const fieldTitle = titlePrefix + (fieldSchema?.title || key); const isConnected = isOutputConnected(nodeId, fullKey); - const shouldShow = isConnected || isOutputVisible; - const { displayType, colorClass, hexColor } = - getTypeDisplayInfo(fieldSchema); const isBroken = brokenOutputs.has(fullKey); const hasNestedProperties = !!fieldSchema?.properties; + const selfIsRelevant = isConnected || isBroken; + const descendantIsRelevant = + hasNestedProperties && + hasConnectedOrBrokenDescendant(fieldSchema.properties!, fullKey); + + const shouldShow = connectedOnly + ? selfIsRelevant || descendantIsRelevant + : isOutputVisible || selfIsRelevant || descendantIsRelevant; + + const { displayType, colorClass, hexColor } = + getTypeDisplayInfo(fieldSchema); const isExpanded = expandedObjects[fullKey] ?? false; + // User expanded → show all children; auto-expanded → filter to connected only + const shouldRenderChildren = isExpanded || descendantIsRelevant; + return shouldShow ? (
- {/* Nested properties: collapsed by default */} + {/* Nested properties */} {hasNestedProperties && - isExpanded && - renderOutputHandles(fieldSchema.properties!, fullKey)} + shouldRenderChildren && + renderOutputHandles( + fieldSchema.properties!, + fullKey, + "", + !isExpanded, + )}
) : null; }, @@ -155,7 +189,7 @@ export const OutputHandler = ({
- {renderOutputHandles(properties)} + {renderOutputHandles(properties, "", "", !isOutputVisible)}
); From 9ed62e95569dcba7e9bd105329ebee0b4c45aa83 Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Tue, 10 Mar 2026 12:06:04 +0530 Subject: [PATCH 06/10] Sync oneOf field state on external discriminator changes Ensure selectedIndex stays synchronized when the discriminator value changes outside the component (e.g., undo/redo, loading saved state). --- .../InputRenderer/base/oneof/OneOfField.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx index 053556766b73..ab43e542a491 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx @@ -144,6 +144,20 @@ function DiscriminatedUnionField({ onChange(newFormData, props.fieldPathId.path, undefined, field_id); } + // Sync selectedIndex when formData discriminator changes externally + // (e.g. undo/redo, loading saved state) + const currentDiscValue = formData?.[discriminatorProp]; + useEffect(() => { + if (!currentDiscValue) return; + const idx = enumOptions.findIndex( + (o) => o.discriminatorValue === currentDiscValue, + ); + if (idx >= 0 && idx !== selectedIndex) { + setSelectedIndex(idx); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentDiscValue]); + // Auto-set discriminator on initial render if missing useEffect(() => { const discValue = enumOptions[selectedIndex]?.discriminatorValue; From cc8ac332ea73ff7efe581927c734a50d4ca9b1aa Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Tue, 10 Mar 2026 12:11:25 +0530 Subject: [PATCH 07/10] Preserve shared fields when switching union variants When changing discriminated union variants, sanitize the current form data against the old schema before applying defaults for the new variant. This preserves any shared fields between variants instead of losing them during the switch. --- .../InputRenderer/base/oneof/OneOfField.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx index ab43e542a491..271dd6693cc2 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx @@ -123,9 +123,8 @@ function DiscriminatedUnionField({ const newIndex = option !== undefined ? parseInt(option, 10) : -1; if (newIndex === selectedIndex || newIndex < 0) return; - setSelectedIndex(newIndex); - const newVariant = variants.current[newIndex]; + const oldVariant = variants.current[selectedIndex]; const discValue = (newVariant.properties?.[discriminatorProp] as any) ?.const; @@ -133,14 +132,22 @@ function DiscriminatedUnionField({ const handlePrefix = cleanUpHandleId(field_id); useEdgeStore.getState().removeEdgesByHandlePrefix(nodeId, handlePrefix); - // Get default form state and set discriminator - let newFormData = schemaUtils.getDefaultFormState( + // Sanitize current data against old→new schema to preserve shared fields + let newFormData = schemaUtils.sanitizeDataForNewSchema( + newVariant, + oldVariant, + formData, + ); + + // Fill in defaults for the new variant + newFormData = schemaUtils.getDefaultFormState( newVariant, - {}, + newFormData, "excludeObjectChildren", ) as any; newFormData = { ...newFormData, [discriminatorProp]: discValue }; + setSelectedIndex(newIndex); onChange(newFormData, props.fieldPathId.path, undefined, field_id); } From 780d12c63b650c03dc5f39a2705818f757940f2c Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Tue, 10 Mar 2026 12:14:16 +0530 Subject: [PATCH 08/10] Fix formatting in OutputHandler node component --- .../build/components/FlowEditor/nodes/OutputHandler.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/OutputHandler.tsx b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/OutputHandler.tsx index ed071ef0b9f7..89045026d6db 100644 --- a/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/OutputHandler.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/build/components/FlowEditor/nodes/OutputHandler.tsx @@ -51,7 +51,10 @@ export const OutputHandler = ({ if (isOutputConnected(nodeId, fullKey) || brokenOutputs.has(fullKey)) return true; if (fieldSchema?.properties) - return hasConnectedOrBrokenDescendant(fieldSchema.properties, fullKey); + return hasConnectedOrBrokenDescendant( + fieldSchema.properties, + fullKey, + ); return false; }, ); From 87a6cf6c2845be4d10527df09cc2e199050134dc Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Tue, 10 Mar 2026 12:16:48 +0530 Subject: [PATCH 09/10] Fix discriminator handling in OneOfField reset logic When the discriminator value becomes unknown or is cleared, reset to the first variant and update formData accordingly, instead of leaving the field in an inconsistent state. --- .../InputRenderer/base/oneof/OneOfField.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx index 271dd6693cc2..40743cd05aad 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx @@ -155,12 +155,24 @@ function DiscriminatedUnionField({ // (e.g. undo/redo, loading saved state) const currentDiscValue = formData?.[discriminatorProp]; useEffect(() => { - if (!currentDiscValue) return; - const idx = enumOptions.findIndex( - (o) => o.discriminatorValue === currentDiscValue, - ); - if (idx >= 0 && idx !== selectedIndex) { - setSelectedIndex(idx); + const idx = currentDiscValue + ? enumOptions.findIndex((o) => o.discriminatorValue === currentDiscValue) + : -1; + + if (idx >= 0) { + if (idx !== selectedIndex) setSelectedIndex(idx); + } else if (enumOptions.length > 0 && selectedIndex !== 0) { + // Unknown or cleared discriminator — reset to first variant + setSelectedIndex(0); + const defaultDisc = enumOptions[0].discriminatorValue; + if (defaultDisc) { + onChange( + { ...formData, [discriminatorProp]: defaultDisc }, + props.fieldPathId.path, + undefined, + field_id, + ); + } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentDiscValue]); From 18f28cc2f1fbd7440670fb4dff6ee26bccf69b95 Mon Sep 17 00:00:00 2001 From: abhi1992002 Date: Tue, 10 Mar 2026 12:33:13 +0530 Subject: [PATCH 10/10] Simplify discriminator reset by reusing handleVariantChange --- .../InputRenderer/base/oneof/OneOfField.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx b/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx index 40743cd05aad..9d96038453f6 100644 --- a/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx +++ b/autogpt_platform/frontend/src/components/renderers/InputRenderer/base/oneof/OneOfField.tsx @@ -162,17 +162,8 @@ function DiscriminatedUnionField({ if (idx >= 0) { if (idx !== selectedIndex) setSelectedIndex(idx); } else if (enumOptions.length > 0 && selectedIndex !== 0) { - // Unknown or cleared discriminator — reset to first variant - setSelectedIndex(0); - const defaultDisc = enumOptions[0].discriminatorValue; - if (defaultDisc) { - onChange( - { ...formData, [discriminatorProp]: defaultDisc }, - props.fieldPathId.path, - undefined, - field_id, - ); - } + // Unknown or cleared discriminator — full reset via same cleanup path + handleVariantChange("0"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentDiscValue]);