Skip to content

feat: Add conversation variable persistence layer #11

Open
tomerqodo wants to merge 21 commits intocoderabbit_only-issues-20260113-coderabbit_completion_base_feat_add_conversation_variable_persistence_layer__pr167from
coderabbit_only-issues-20260113-coderabbit_completion_head_feat_add_conversation_variable_persistence_layer__pr167
Open

feat: Add conversation variable persistence layer #11
tomerqodo wants to merge 21 commits intocoderabbit_only-issues-20260113-coderabbit_completion_base_feat_add_conversation_variable_persistence_layer__pr167from
coderabbit_only-issues-20260113-coderabbit_completion_head_feat_add_conversation_variable_persistence_layer__pr167

Conversation

@tomerqodo
Copy link

@tomerqodo tomerqodo commented Jan 18, 2026

Benchmark PR from qodo-benchmark#167

Summary by CodeRabbit

  • New Features

    • Added conversation variable persistence to workflow execution, enabling variables to be properly stored and updated during workflow runs.
  • Refactor

    • Restructured variable persistence architecture to centralize variable updates and improve workflow state management.
  • Tests

    • Added comprehensive test coverage for the new variable persistence layer and updated existing tests to validate workflow variable handling.

✏️ Tip: You can customize this high-level summary in your review settings.

laipz8200 and others added 21 commits January 5, 2026 13:19
… factory to pass the ConversationVariableUpdater factory (the only non-VariablePool dependency), plus a unit test to verify the injection path.

- `api/core/workflow/nodes/variable_assigner/v2/node.py` adds a kw-only `conv_var_updater_factory` dependency (defaulting to `conversation_variable_updater_factory`) and stores it for use in `_run`.
- `api/core/workflow/nodes/node_factory.py` now injects the factory when creating VariableAssigner v2 nodes.
- `api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_variable_assigner_v2.py` adds a test asserting the factory is injected.

Tests not run.

Next steps (optional):
1) `make lint`
2) `make type-check`
3) `uv run --project api --dev dev/pytest/pytest_unit_tests.sh`
…ructor args.

- `api/core/workflow/nodes/node_factory.py` now directly instantiates `VariableAssignerNode` with the injected dependency, and uses a direct call for all other nodes.

No tests run.
Add a new command for GraphEngine to update a group of variables. This command takes a group of variable selectors and new values. When the engine receives the command, it will update the corresponding variable in the variable pool. If it does not exist, it will add it; if it does, it will overwrite it. Both behaviors should be treated the same and do not need to be distinguished.
…be-kanban 0941477f)

Create a new persistence layer for the Graph Engine. This layer receives a ConversationVariableUpdater upon initialization, which is used to persist the received ConversationVariables to the database. It can retrieve the currently processing ConversationId from the engine's variable pool. It captures the successful execution event of each node and determines whether the type of this node is VariableAssigner(v1 and v2). If so, it retrieves the variable name and value that need to be updated from the node's outputs. This layer is only used in the Advanced Chat. It should be placed outside of Core.Workflow package.
…rs/conversation_variable_persist_layer.py` to satisfy SIM118

- chore(lint): run `make lint` (passes; warnings about missing RECORD during venv package uninstall)
- chore(type-check): run `make type-check` (fails: 1275 errors for missing type stubs like `opentelemetry`, `click`, `sqlalchemy`, `flask`, `pydantic`, `pydantic_settings`)
…tType validation and casting

- test(graph-engine): update VariableUpdate usages to include value_type in command tests
… drop common_helpers usage

- refactor(variable-assigner-v2): inline updated variable payload and drop common_helpers usage

Tests not run.
…n and remove value type validation

- test(graph-engine): update UpdateVariablesCommand tests to pass concrete Variable instances
- fix(graph-engine): align VariableUpdate values with selector before adding to VariablePool

Tests not run.
…e handling for v1/v2 process_data

- refactor(app-layer): read updated variables from process_data in conversation variable persistence layer
- test(app-layer): adapt persistence layer tests to use common_helpers updated-variable payloads

Tests not run.
…fter venv changes)

- chore(type-check): run `make type-check` (fails: 1275 missing type stubs across dependencies)

Details:
- `make lint` fails with `ModuleNotFoundError: No module named 'dotenv_linter.cli'`.
- `make type-check` fails with missing stubs for `opentelemetry`, `click`, `sqlalchemy`, `flask`, `pydantic`, `pydantic_settings`, etc.
…ableUnion and remove value type validation"

This reverts commit 5ebc87a.
…h SegmentType validation and casting"

This reverts commit 3edd525.
…y out of core.workflow into `api/services/conversation_variable_updater.py`

- refactor(app): update advanced chat app runner and conversation service to import the new updater factory

Tests not run.
…-linter module missing)

- chore(type-check): run `make type-check` (fails: 1275 missing type stubs)

Details:
- `make lint` reports: `No matches for ignored import core.workflow.nodes.variable_assigner.common.impl -> extensions.ext_database` and ends with `ModuleNotFoundError: No module named 'dotenv_linter.cli'`.
- `make type-check` fails with missing type stubs for `opentelemetry`, `click`, `sqlalchemy`, `flask`, `pydantic`, `pydantic_settings`, etc.
@coderabbitai
Copy link

coderabbitai bot commented Jan 18, 2026

Walkthrough

This PR refactors conversation variable persistence from node-level database updates to an event-driven persistence layer. It moves ConversationVariableUpdaterImpl to a service module, introduces ConversationVariablePersistenceLayer as a workflow event listener, updates the variable pool interface to use selector-based lookups, and removes direct persistence logic from variable assigner nodes.

Changes

Cohort / File(s) Summary
Import Rules
api/.importlinter, api/core/workflow/nodes/node_factory.py
Removed ignore rule for core.workflow.nodes.variable_assigner.common.implextensions.ext_database import; minor whitespace cleanup.
Persistence Layer Implementation
api/core/app/layers/conversation_variable_persist_layer.py, api/services/conversation_variable_updater.py, api/core/app/apps/advanced_chat/app_runner.py
Added new ConversationVariablePersistenceLayer that listens for NodeRunSucceededEvent from variable assigners and enqueues variable updates. Moved ConversationVariableUpdaterImpl and conversation_variable_updater_factory to service module with queue-based batch flushing. Integrated layer into AdvancedChatAppRunner.
Variable Assigner Refactoring
api/core/workflow/nodes/variable_assigner/common/impl.py, api/core/workflow/nodes/variable_assigner/v1/node.py, api/core/workflow/nodes/variable_assigner/v2/node.py
Removed ConversationVariableUpdaterImpl and updater factory from common impl; removed conv_var_updater_factory parameter and logic from v1 node; added explicit __init__ with dependency injection and removed updater integration from v2 node.
Variable Pool Interface
api/core/workflow/runtime/graph_runtime_state_protocol.py, api/core/workflow/runtime/read_only_wrappers.py
Updated ReadOnlyVariablePool.get() signature from (node_id: str, variable_key: str) to (selector: Sequence[str], /) for selector-based variable lookups.
Service Configuration
api/services/conversation_service.py
Updated import path for conversation_variable_updater_factory from workflow impl to service module.
Tests
api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py, api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py, api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py, api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_variable_assigner_v2.py
Added new test suite for persistence layer event handling; updated mock variable pool to match selector-based interface; refactored v1 tests to use event-based assertions instead of updater mocks; added v2 node factory test.

Sequence Diagram

sequenceDiagram
    participant VA as VariableAssigner<br/>Node
    participant Graph as Workflow Graph
    participant Layer as Conversation<br/>VariablePersistence<br/>Layer
    participant Updater as Conversation<br/>VariableUpdater
    participant DB as Database
    
    VA->>Graph: emit NodeRunSucceededEvent<br/>(with process_data)
    Graph->>Layer: on_event(NodeRunSucceededEvent)
    activate Layer
    Layer->>Layer: extract conversation_id<br/>from system_variables
    Layer->>Layer: validate selector<br/>refers to conversation var
    Layer->>Graph: variable_pool.get(selector)
    Graph-->>Layer: Variable instance
    Layer->>Updater: update(conversation_id,<br/>variable)
    deactivate Layer
    Note over Updater: enqueue update
    
    Graph->>Layer: on_graph_end()
    activate Layer
    Layer->>Updater: flush()
    activate Updater
    loop for each enqueued update
        Updater->>DB: query ConversationVariable<br/>by id and conversation_id
        DB-->>Updater: result
        Updater->>DB: update row data
        DB-->>Updater: commit
    end
    deactivate Updater
    deactivate Layer
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 ✨ Events now flow like carrots in the stream,
Persistence blooms where layers scheme,
Variables dance through selectors bright,
From nodes to services, refactored just right!
A workflow reborn, oh what a dream! 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal (single line) and does not follow the repository's template with summary, checklist, or proper issue linking. Fill out the description template with a proper summary of changes, link to the associated issue using 'Fixes #', and complete the provided checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a conversation variable persistence layer to the system.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@api/core/workflow/nodes/variable_assigner/v1/node.py`:
- Line 91: The code calls common_helpers.variable_to_processed_data with
original_variable instead of the newly updated variable, causing stale values;
update the call in the v1 flow to pass the updated variable (updated_variable)
or retrieve the variable from the pool after the write (as v2 does) before
calling variable_to_processed_data; locate the creation of updated_variable (and
the updated_variables list) and replace original_variable with updated_variable
(or pool.fetch the variable and pass that result) so variable_to_processed_data
receives the up-to-date value.

In `@api/services/conversation_variable_updater.py`:
- Around line 13-31: The flush method in ConversationVariableUpdaterImpl replays
prior updates because it never clears self._pending_updates; modify the flush
method to clear or reset self._pending_updates (e.g., assign [] or call clear())
after successfully iterating/committing updates (and preferably in a finally
block to ensure clearing even on errors) so repeated calls to flush do not
duplicate previous writes.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3e19c4 and 84196e6.

📒 Files selected for processing (15)
  • api/.importlinter
  • api/core/app/apps/advanced_chat/app_runner.py
  • api/core/app/layers/conversation_variable_persist_layer.py
  • api/core/workflow/nodes/node_factory.py
  • api/core/workflow/nodes/variable_assigner/common/impl.py
  • api/core/workflow/nodes/variable_assigner/v1/node.py
  • api/core/workflow/nodes/variable_assigner/v2/node.py
  • api/core/workflow/runtime/graph_runtime_state_protocol.py
  • api/core/workflow/runtime/read_only_wrappers.py
  • api/services/conversation_service.py
  • api/services/conversation_variable_updater.py
  • api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py
  • api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py
  • api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py
  • api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_variable_assigner_v2.py
💤 Files with no reviewable changes (3)
  • api/core/workflow/nodes/node_factory.py
  • api/core/workflow/nodes/variable_assigner/common/impl.py
  • api/.importlinter
🧰 Additional context used
🧬 Code graph analysis (9)
api/core/app/apps/advanced_chat/app_runner.py (3)
api/core/app/layers/conversation_variable_persist_layer.py (1)
  • ConversationVariablePersistenceLayer (14-56)
api/services/conversation_variable_updater.py (1)
  • conversation_variable_updater_factory (33-34)
api/core/workflow/graph_engine/graph_engine.py (1)
  • layer (215-218)
api/core/workflow/runtime/graph_runtime_state_protocol.py (3)
api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py (1)
  • get (22-25)
api/core/workflow/runtime/read_only_wrappers.py (1)
  • get (21-24)
api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py (1)
  • get (70-78)
api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_variable_assigner_v2.py (2)
api/core/workflow/nodes/node_factory.py (2)
  • DifyNodeFactory (29-130)
  • create_node (66-130)
api/core/workflow/nodes/variable_assigner/v2/node.py (1)
  • VariableAssignerNode (53-248)
api/core/workflow/nodes/variable_assigner/v2/node.py (4)
api/core/workflow/enums.py (1)
  • NodeType (36-85)
api/core/workflow/nodes/base/node.py (2)
  • Node (61-721)
  • graph_init_params (244-245)
api/core/workflow/runtime/graph_runtime_state.py (1)
  • GraphRuntimeState (132-478)
api/tests/unit_tests/core/workflow/nodes/llm/test_node.py (2)
  • graph_init_params (73-83)
  • graph_runtime_state (87-95)
api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py (6)
api/core/app/layers/conversation_variable_persist_layer.py (1)
  • on_event (22-53)
api/core/workflow/enums.py (1)
  • NodeType (36-85)
api/core/workflow/graph_events/node.py (1)
  • NodeRunSucceededEvent (38-39)
api/core/workflow/runtime/graph_runtime_state_protocol.py (8)
  • ReadOnlyGraphRuntimeState (25-83)
  • system_variable (35-35)
  • get (12-14)
  • get_all_by_node (16-18)
  • get_by_prefix (20-22)
  • variable_pool (38-40)
  • outputs (58-60)
  • start_at (43-45)
api/core/workflow/runtime/read_only_wrappers.py (7)
  • system_variable (47-48)
  • get (21-24)
  • get_all_by_node (26-32)
  • get_by_prefix (34-36)
  • variable_pool (51-52)
  • outputs (67-68)
  • start_at (55-56)
api/core/workflow/system_variable.py (3)
  • SystemVariable (11-118)
  • conversation_id (159-160)
  • as_view (117-118)
api/services/conversation_service.py (1)
api/services/conversation_variable_updater.py (1)
  • conversation_variable_updater_factory (33-34)
api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py (3)
api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py (1)
  • get (22-25)
api/core/workflow/runtime/graph_runtime_state_protocol.py (1)
  • get (12-14)
api/core/workflow/runtime/read_only_wrappers.py (1)
  • get (21-24)
api/core/workflow/runtime/read_only_wrappers.py (3)
api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py (1)
  • get (22-25)
api/core/workflow/runtime/graph_runtime_state_protocol.py (1)
  • get (12-14)
api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py (1)
  • get (70-78)
api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py (2)
api/core/workflow/graph_events/node.py (1)
  • NodeRunSucceededEvent (38-39)
api/core/workflow/nodes/base/node.py (1)
  • run (267-349)
🔇 Additional comments (24)
api/core/workflow/nodes/variable_assigner/v2/node.py (3)

25-28: LGTM! Proper use of TYPE_CHECKING for forward references.

Using TYPE_CHECKING to import GraphInitParams and GraphRuntimeState avoids circular imports at runtime while maintaining type safety for static analysis.


56-68: LGTM! Constructor properly delegates to base class.

The explicit __init__ correctly passes all required parameters to the parent Node class, aligning with the base class signature per the relevant context.


108-211: LGTM! Clean separation of validation and execution logic.

The _run method properly:

  1. Validates variables before modification
  2. Updates the variable pool after successful operations
  3. Handles deduplication of updated selectors (line 191)
  4. Constructs appropriate NodeRunResult with process_data for downstream consumers

The removal of direct ConversationVariableUpdater integration aligns with the PR's shift to event-driven persistence via ConversationVariablePersistenceLayer.

api/core/workflow/nodes/variable_assigner/v1/node.py (1)

22-34: LGTM! Constructor aligned with v2 pattern.

The constructor properly delegates to the base Node class with all required parameters.

api/core/app/apps/advanced_chat/app_runner.py (3)

23-23: LGTM! New persistence layer import.

The import correctly references the new ConversationVariablePersistenceLayer from the layers module.


44-44: LGTM! Factory import from services module.

The import path correctly references the relocated conversation_variable_updater_factory in the services module.


204-206: LGTM! Conversation variable persistence layer integration.

The ConversationVariablePersistenceLayer is correctly instantiated with a fresh updater from the factory and added to the graph engine before the general WorkflowPersistenceLayer. This ensures conversation variable updates are captured and persisted during workflow execution via the event-driven pattern.

api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_variable_assigner_v2.py (1)

395-431: LGTM! Good test coverage for node factory integration.

The test verifies that DifyNodeFactory correctly creates a VariableAssignerNode instance from a configuration with version: "2". This ensures the factory-to-node wiring works correctly after the constructor changes.

api/services/conversation_service.py (1)

20-20: LGTM! Import path updated to reflect module relocation.

The import correctly references the new location of conversation_variable_updater_factory in the services module, consistent with the PR's refactoring to move the updater implementation out of core.workflow.

api/core/workflow/runtime/read_only_wrappers.py (2)

3-3: LGTM! Added Sequence import for new selector type.

The import supports the updated method signature using Sequence[str] for selectors.


21-24: LGTM! Signature updated to match protocol.

The get method signature now accepts a Sequence[str] selector, aligning with the ReadOnlyVariablePool protocol defined in graph_runtime_state_protocol.py. The positional-only parameter (/) enforces the calling convention and the deepcopy preserves read-only semantics.

api/core/workflow/runtime/graph_runtime_state_protocol.py (1)

12-12: Selector-based accessor signature looks consistent.

The updated signature matches the new selector-based access pattern.

api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py (7)

18-31: Mock variable pool selector handling is aligned.

The selector guard and tuple lookup mirror the new pool access pattern.


34-41: Graph runtime state helper is clean and focused.

Simple setup for variable pool + system variable is sufficient.


44-60: Event builder is straightforward and reusable.

Encapsulates node run result creation cleanly.


63-85: Covers the happy path clearly.

Assertions validate both update and flush behavior as expected.


88-108: Good negative case for missing outputs.

Skips are verified without updater interaction.


110-119: Non-assigner nodes are correctly ignored.

Test aligns with layer filtering.


122-144: Non-conversation selector case is validated.

Ensures environment-scoped variables are skipped.

api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py (1)

70-78: Selector-based get implementation is consistent.

Guard + tuple access aligns with new ReadOnlyVariablePool.get contract.

api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py (3)

106-111: Event-driven assertions look solid.

The test now validates updated variable payloads from the node event.


202-207: Append flow assertions are correct.

Validates updated variable data via the event process_data.


287-292: Clear flow assertions are correct.

Event-driven update payload check matches the new behavior.

api/core/app/layers/conversation_variable_persist_layer.py (1)

22-53: No issue—the pool guarantees Variable instances despite the broad type annotation.

The VariablePool.get() method is annotated to return Segment | None, but its implementation always stores and returns Variable instances. The add() method converts any input (Variable, Segment, or raw value) to a Variable before storage using segment_to_variable(), and the pool's type is VariableUnion, which is a union of all Variable subclasses. The isinstance(variable, Variable) check in the layer will always pass for any non-None result, so persistence will not be silently skipped. The type annotation is conservatively broad compared to the runtime guarantee, but this is not a functional defect.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

conv_var_updater.flush()
updated_variables = [common_helpers.variable_to_processed_data(assigned_variable_selector, updated_variable)]

updated_variables = [common_helpers.variable_to_processed_data(assigned_variable_selector, original_variable)]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "node.py" | grep "variable_assigner" | head -20

Repository: qodo-benchmark/dify-combined-coderabbit

Length of output: 192


🏁 Script executed:

git ls-files | grep -E "variable_assigner.*node\.py"

Repository: qodo-benchmark/dify-combined-coderabbit

Length of output: 188


🏁 Script executed:

sed -n '80,100p' api/core/workflow/nodes/variable_assigner/v1/node.py

Repository: qodo-benchmark/dify-combined-coderabbit

Length of output: 1329


🏁 Script executed:

sed -n '190,210p' api/core/workflow/nodes/variable_assigner/v2/node.py

Repository: qodo-benchmark/dify-combined-coderabbit

Length of output: 1049


🏁 Script executed:

rg -n "def variable_to_processed_data" --type py

Repository: qodo-benchmark/dify-combined-coderabbit

Length of output: 235


🏁 Script executed:

rg -B3 -A10 "def variable_to_processed_data" --type py

Repository: qodo-benchmark/dify-combined-coderabbit

Length of output: 1342


🏁 Script executed:

sed -n '20,40p' api/core/workflow/nodes/variable_assigner/common/helpers.py

Repository: qodo-benchmark/dify-combined-coderabbit

Length of output: 635


Pass updated_variable to variable_to_processed_data instead of original_variable.

Line 91 passes original_variable to variable_to_processed_data, but updated_variable (created on line 89) contains the new value that was just written to the pool. Since variable_to_processed_data extracts the .value property to populate new_value in the returned UpdatedVariable, using original_variable causes stale data to appear in process_data.

The v2 implementation (lines 194-197) correctly fetches the updated variable from the pool after the update before passing it to variable_to_processed_data. Either pass updated_variable directly or fetch from the pool to match v2's approach.

🤖 Prompt for AI Agents
In `@api/core/workflow/nodes/variable_assigner/v1/node.py` at line 91, The code
calls common_helpers.variable_to_processed_data with original_variable instead
of the newly updated variable, causing stale values; update the call in the v1
flow to pass the updated variable (updated_variable) or retrieve the variable
from the pool after the write (as v2 does) before calling
variable_to_processed_data; locate the creation of updated_variable (and the
updated_variables list) and replace original_variable with updated_variable (or
pool.fetch the variable and pass that result) so variable_to_processed_data
receives the up-to-date value.

Comment on lines +13 to +31
class ConversationVariableUpdaterImpl:
def __init__(self) -> None:
self._pending_updates: list[tuple[str, Variable]] = []

def update(self, conversation_id: str, variable: Variable) -> None:
self._pending_updates.append((conversation_id, variable))

def flush(self) -> None:
for conversation_id, variable in self._pending_updates:
stmt = select(ConversationVariable).where(
ConversationVariable.id == variable.id, ConversationVariable.conversation_id == conversation_id
)
with Session(db.engine) as session:
row = session.scalar(stmt)
if not row:
raise ConversationVariableNotFoundError("conversation variable not found in the database")
row.data = variable.model_dump_json()
session.commit()

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear pending updates after flush to avoid duplicate writes.

_pending_updates is never cleared, so every flush replays prior updates; with per-update flushes this can cause repeated DB writes.

🛠️ Proposed fix
     def flush(self) -> None:
         for conversation_id, variable in self._pending_updates:
             stmt = select(ConversationVariable).where(
                 ConversationVariable.id == variable.id, ConversationVariable.conversation_id == conversation_id
             )
             with Session(db.engine) as session:
                 row = session.scalar(stmt)
                 if not row:
                     raise ConversationVariableNotFoundError("conversation variable not found in the database")
                 row.data = variable.model_dump_json()
                 session.commit()
+        self._pending_updates.clear()
🤖 Prompt for AI Agents
In `@api/services/conversation_variable_updater.py` around lines 13 - 31, The
flush method in ConversationVariableUpdaterImpl replays prior updates because it
never clears self._pending_updates; modify the flush method to clear or reset
self._pending_updates (e.g., assign [] or call clear()) after successfully
iterating/committing updates (and preferably in a finally block to ensure
clearing even on errors) so repeated calls to flush do not duplicate previous
writes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants