Skip to content

fix(memory): stop cross-flow chat-history leak in legacy flows (LE-1929) - #14191

Merged
Cristhianzl merged 3 commits into
release-1.11.0from
cz/fix-memory-backward-leak
Jul 22, 2026
Merged

fix(memory): stop cross-flow chat-history leak in legacy flows (LE-1929)#14191
Cristhianzl merged 3 commits into
release-1.11.0from
cz/fix-memory-backward-leak

Conversation

@Cristhianzl

@Cristhianzl Cristhianzl commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

Fixes a cross-flow chat-history leak (issue #13059) that still affected legacy saved flows, plus two flow-editor rendering bugs surfaced while validating the same legacy flow.

1. Memory — cross-flow chat-history leak in legacy flows (primary)

MemoryComponent.retrieve_messages was scoped by flow_id in #13087, but Langflow executes the frozen component code embedded in each saved flow (lfx.interface.initialize.loading.instantiate_classeval_custom_component_code), not the installed library. A flow saved before #13087 keeps the old unscoped retrieve_messages and leaks another flow's history on a colliding session_id — on any server version.

Fix (defense-in-depth; frozen code untouched): an ambient flow_id ContextVar (lfx/memory/flow_context.py) is bound around component execution in get_instance_results; aget_messages defaults its scope from it only when flow_id is None. Explicit callers and non-graph callers are unchanged — this extends the #13087 contract to the platform function the frozen code calls, instead of the (unchangeable) frozen code.

Verified live on the reporter's actual legacy flow across POST /api/v1/run/{flow} and POST /api/v1/build/{flow}/flow (Playground): leaked before, no leak after.

2. Frontend — cleanEdges skips the source-handle migration when both handles migrate

When an edge needs both handles migrated (e.g. DataFrameTable, #11554), the target block rewrote edge.id, then the source block's find-by-original-id failed and silently skipped the source rewrite — leaving a kept-but-detached edge. Fixed by capturing the edge reference once and reusing it in both blocks.

3. Frontend — legacy nodes render with detached edges (stale width)

Flows saved when the node card was w-96 (384px) kept width: 384; the card now renders w-80 (320px), so React Flow placed the right-side output handle ~64px past the visual dot (edges rendered detached, unfixable by dragging). processFlowNodes now drops persisted width/height/measured on genericNodes so React Flow re-measures the real size; noteNodes (user-resizable) are preserved.

Testing

  • Backend: 6 new tests in test_message_flow_context_scoping.py (incl. a graph-level e2e reproducing an old-style unscoped component); 115 existing memory tests green; ruff format/check clean.
  • Frontend: updatePathEdgeMigration.test.ts, reconcileNodeDimensions.test.ts; 40 existing edge/type-compatibility tests green; biome clean.

Backward compatibility

No component class or name/input/output renames. Changes are additive and default to legacy behavior when the new context is absent, so existing flows and API consumers are unaffected.

Summary by CodeRabbit

  • Bug Fixes

    • Chat memory retrieval is now correctly scoped to the active flow, preventing messages from other flows with the same session from appearing.
    • Explicit flow selections continue to override automatic scoping, while invalid or missing context preserves legacy behavior.
    • Fixed migration of saved connections when output types change from DataFrame to Table.
    • Generic nodes now refresh stale dimensions correctly while preserving user-resized note dimensions.
  • Tests

    • Added regression coverage for flow-scoped memory retrieval, graph execution, edge migration, and node dimension reconciliation.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f2a9a376-5081-4d2d-b8f6-c9b3293b7747

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Changes

The PR adds ambient flow scoping for backend message retrieval, binds and resets flow context during graph component execution, and adds regression coverage. It also fixes React Flow edge migration references and stale generic-node dimension handling.

Ambient flow-scoped message retrieval

Layer / File(s) Summary
Flow context contract
src/lfx/src/lfx/memory/flow_context.py
Adds ContextVar helpers for reading, binding, resetting, and coercing the current flow identifier.
Execution binding and retrieval fallback
src/lfx/src/lfx/interface/initialize/loading.py, src/backend/base/langflow/memory.py
Graph component execution binds the flow identifier, and aget_messages derives it when no explicit identifier is provided.
Flow-scoping regression coverage
src/backend/tests/unit/test_message_flow_context_scoping.py
Tests scoped retrieval, explicit overrides, legacy fallback, invalid values, and old-style component execution.

React Flow persistence fixes

Layer / File(s) Summary
Stable edge migration cleanup
src/frontend/src/utils/reactflowUtils.ts, src/frontend/src/utils/__tests__/updatePathEdgeMigration.test.ts
Edge cleanup removes broken edges by retained reference and verifies legacy DataFrame handles migrate to Table.
Generic-node dimension reconciliation
src/frontend/src/utils/reactflowUtils.ts, src/frontend/src/utils/__tests__/reconcileNodeDimensions.test.ts
Generic-node dimensions are cleared for remeasurement while note-node dimensions remain unchanged.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Graph
  participant ComponentExecution
  participant MessageRetrieval
  participant MessageStore
  Graph->>ComponentExecution: execute graph component
  ComponentExecution->>ComponentExecution: bind graph flow_id
  ComponentExecution->>MessageRetrieval: retrieve messages without flow_id
  MessageRetrieval->>MessageStore: query session and ambient flow_id
  MessageStore-->>MessageRetrieval: scoped messages
  ComponentExecution->>ComponentExecution: reset flow context
Loading

Possibly related PRs

Suggested labels: bug

Suggested reviewers: jordanrfrazier

🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (8 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Coverage For New Implementations ✅ Passed Each code change has matching regression tests—backend scoping unit/e2e, frontend edge-migration and dimension unit tests—and filenames follow conventions.
Test Quality And Coverage ✅ Passed Backend tests cover default/override/legacy/invalid flow scoping plus an end-to-end graph run; frontend unit tests hit the real utilities and match repo pytest/Jest patterns.
Test File Naming And Structure ✅ Passed New backend tests are test_*.py pytest-style unit tests; frontend tests are .test.ts Jest unit tests in tests, with descriptive names and edge/negative cases.
Excessive Mock Usage Warning ✅ Passed The new tests use real production functions and objects directly; no explicit mocks, stubs, or monkeypatching were found.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main backend fix: preventing cross-flow chat-history leakage in legacy flows.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cz/fix-memory-backward-leak

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bug Something isn't working label Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Test Coverage Advisor

No source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉

Advisory check only — never blocks merge.

@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/src/utils/reactflowUtils.ts`:
- Around line 128-130: Update the source-invalid removal paths in the edge
migration flow to use the retained edge reference `edgeInNewEdges` for filtering
and for the `filterHiddenFieldsEdges` call, rather than the original `edge`.
Ensure every later removal reflects any migrated `edgeInNewEdges.id` so broken
or hidden edges are actually removed from `newEdges`.

In `@src/lfx/src/lfx/interface/initialize/loading.py`:
- Around line 81-94: Update the flow-scope setup around build_custom_component
and build_component so set_current_flow_id is always called, passing flow_id
even when it is None. Continue resetting the returned token in the finally
block, preserving unscoped behavior when vertex.graph.flow_id is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a01aa0a8-7dcb-4af4-899e-c32fd7c31ce7

📥 Commits

Reviewing files that changed from the base of the PR and between f685d29 and 4ac2566.

📒 Files selected for processing (7)
  • src/backend/base/langflow/memory.py
  • src/backend/tests/unit/test_message_flow_context_scoping.py
  • src/frontend/src/utils/__tests__/reconcileNodeDimensions.test.ts
  • src/frontend/src/utils/__tests__/updatePathEdgeMigration.test.ts
  • src/frontend/src/utils/reactflowUtils.ts
  • src/lfx/src/lfx/interface/initialize/loading.py
  • src/lfx/src/lfx/memory/flow_context.py

Comment on lines +128 to +130
// Hold by reference: the target block rewrites edge.id on migration, so a second find-by-id in
// the source block would miss it and skip the source-handle migration (DataFrame stays). LE-1929.
const edgeInNewEdges = newEdges.find((e) => e.id === edge.id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the retained edge reference for every later removal.

After a handle migration rewrites edgeInNewEdges.id, the source-invalid paths still filter by the original edge.id (Lines 282 and 306), and filterHiddenFieldsEdges receives that stale edge (Line 312). Those paths then report a broken/hidden edge but leave it in newEdges.

Proposed fix
- newEdges = newEdges.filter((e) => e.id !== edge.id);
+ newEdges = newEdges.filter((e) => e !== edgeInNewEdges);

- newEdges = filterHiddenFieldsEdges(edge, newEdges, targetNode);
+ newEdges = filterHiddenFieldsEdges(
+   edgeInNewEdges ?? edge,
+   newEdges,
+   targetNode,
+ );

Also applies to: 205-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/utils/reactflowUtils.ts` around lines 128 - 130, Update the
source-invalid removal paths in the edge migration flow to use the retained edge
reference `edgeInNewEdges` for filtering and for the `filterHiddenFieldsEdges`
call, rather than the original `edge`. Ensure every later removal reflects any
migrated `edgeInNewEdges.id` so broken or hidden edges are actually removed from
`newEdges`.

Comment on lines +81 to +94
flow_id = getattr(getattr(vertex, "graph", None), "flow_id", None)
flow_scope_token = set_current_flow_id(flow_id) if flow_id is not None else None
try:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20)
if base_type == "custom_components":
return await build_custom_component(params=custom_params, custom_component=custom_component)
if base_type == "component":
return await build_component(params=custom_params, custom_component=custom_component)
msg = f"Base type {base_type} not found."
raise ValueError(msg)
finally:
if flow_scope_token is not None:
reset_current_flow_id(flow_scope_token)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Always shadow the ambient context, including with None.

Line 82 leaves a previously bound flow scope active when this vertex has no graph.flow_id. Nested execution can then retrieve messages under the outer flow’s ID instead of preserving the intended unscoped behavior.

Proposed fix
 flow_id = getattr(getattr(vertex, "graph", None), "flow_id", None)
-flow_scope_token = set_current_flow_id(flow_id) if flow_id is not None else None
+flow_scope_token = set_current_flow_id(flow_id)
 try:
     ...
 finally:
-    if flow_scope_token is not None:
-        reset_current_flow_id(flow_scope_token)
+    reset_current_flow_id(flow_scope_token)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
flow_id = getattr(getattr(vertex, "graph", None), "flow_id", None)
flow_scope_token = set_current_flow_id(flow_id) if flow_id is not None else None
try:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20)
if base_type == "custom_components":
return await build_custom_component(params=custom_params, custom_component=custom_component)
if base_type == "component":
return await build_component(params=custom_params, custom_component=custom_component)
msg = f"Base type {base_type} not found."
raise ValueError(msg)
finally:
if flow_scope_token is not None:
reset_current_flow_id(flow_scope_token)
flow_id = getattr(getattr(vertex, "graph", None), "flow_id", None)
flow_scope_token = set_current_flow_id(flow_id)
try:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20)
if base_type == "custom_components":
return await build_custom_component(params=custom_params, custom_component=custom_component)
if base_type == "component":
return await build_component(params=custom_params, custom_component=custom_component)
msg = f"Base type {base_type} not found."
raise ValueError(msg)
finally:
reset_current_flow_id(flow_scope_token)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lfx/src/lfx/interface/initialize/loading.py` around lines 81 - 94, Update
the flow-scope setup around build_custom_component and build_component so
set_current_flow_id is always called, passing flow_id even when it is None.
Continue resetting the returned token in the finally block, preserving unscoped
behavior when vertex.graph.flow_id is absent.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.30%. Comparing base (685c249) to head (a7b39b2).
⚠️ Report is 2 commits behind head on release-1.11.0.

Files with missing lines Patch % Lines
src/lfx/src/lfx/memory/flow_context.py 52.63% 9 Missing ⚠️
src/lfx/src/lfx/interface/initialize/loading.py 61.53% 3 Missing and 2 partials ⚠️
src/frontend/src/utils/reactflowUtils.ts 96.66% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##           release-1.11.0   #14191      +/-   ##
==================================================
+ Coverage           60.84%   61.30%   +0.45%     
==================================================
  Files                2441     2442       +1     
  Lines              238902   238980      +78     
  Branches            36073    36123      +50     
==================================================
+ Hits               145368   146497    +1129     
+ Misses              91778    90727    -1051     
  Partials             1756     1756              
Flag Coverage Δ
backend 68.81% <100.00%> (+2.52%) ⬆️
frontend 59.63% <96.66%> (+0.01%) ⬆️
lfx 59.88% <56.25%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/backend/base/langflow/memory.py 72.62% <100.00%> (+1.03%) ⬆️
src/frontend/src/utils/reactflowUtils.ts 51.82% <96.66%> (+1.42%) ⬆️
src/lfx/src/lfx/interface/initialize/loading.py 41.51% <61.53%> (+1.33%) ⬆️
src/lfx/src/lfx/memory/flow_context.py 52.63% <52.63%> (ø)

... and 181 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@erichare
erichare self-requested a review July 21, 2026 18:40

@erichare erichare left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple things @Cristhianzl

Blocking CI: Biome reports seven noExplicitAny errors in the new frontend tests. The Docker job was canceled after a runner shutdown, not a demonstrated Docker regression; other checks remain pending.

Validation: PR backend tests 6 passed; frontend tests 3 passed; git diff --check and synthetic merge-tree clean.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 47%
47.04% (66931/142268) 70.13% (9397/13398) 45.36% (1532/3377)

Unit Test Results

Tests Skipped Failures Errors Time
5349 0 💤 0 ❌ 0 🔥 18m 48s ⏱️

@erichare
erichare self-requested a review July 21, 2026 19:18
@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Jul 21, 2026

@erichare erichare left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@github-actions github-actions Bot added the lgtm This PR has been approved by a maintainer label Jul 21, 2026
Cristhianzl and others added 2 commits July 21, 2026 16:18
A graph without flow_id must run legacy-unscoped, not inherit an outer
flow's ambient scope. Bind the ContextVar unconditionally (including
None) and reset it unconditionally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013CgXpMiyLTFSEueEzcrync
After the target migration rewrites the retained edge's id, the
source-invalid and hidden-field removal paths still filtered by the
original id and silently kept broken edges. Filter by the retained
reference and pass it to filterHiddenFieldsEdges. Also type the new
test fixtures (biome noExplicitAny).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013CgXpMiyLTFSEueEzcrync
@Cristhianzl
Cristhianzl added this pull request to the merge queue Jul 22, 2026
Merged via the queue into release-1.11.0 with commit ae7c292 Jul 22, 2026
164 checks passed
@Cristhianzl
Cristhianzl deleted the cz/fix-memory-backward-leak branch July 22, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants