Skip to content

fix(data): prevent empty string from overwriting step content on upsert#2807

Merged
hayescode merged 1 commit intoChainlit:mainfrom
giulio-leone:fix/step-race-condition-empty-string-overwrite
Mar 5, 2026
Merged

fix(data): prevent empty string from overwriting step content on upsert#2807
hayescode merged 1 commit intoChainlit:mainfrom
giulio-leone:fix/step-race-condition-empty-string-overwrite

Conversation

@giulio-leone
Copy link
Contributor

@giulio-leone giulio-leone commented Feb 28, 2026

Problem

When using cl.Step, content within steps occasionally disappears or appears empty when reloading a thread from history. This is caused by a race condition between the initial step.send() and the subsequent step.update().

cl.Step initializes input and output as empty strings (""). In ChainlitDataLayer.create_step, the SQL uses:

output = COALESCE(EXCLUDED.output, "Step".output)

Since empty string is not NULL, COALESCE accepts it as a valid value. If the background task for the initial send() (with empty output) finishes after the final update() (with actual content), the empty string overwrites the content.

Fix

Wrap EXCLUDED.output and EXCLUDED.input with NULLIF(..., ''):

output = COALESCE(NULLIF(EXCLUDED.output, ''), "Step".output)
input = COALESCE(NULLIF(EXCLUDED.input, ''), "Step".input)

This treats empty strings as NULL, so COALESCE falls back to the existing database content.

Test

Added test_create_step_uses_nullif_for_output_and_input that verifies the SQL contains the NULLIF protection.

Fixes #2789


Summary by cubic

Prevent empty-string input/output from overwriting step content during upsert. Fixes a race where initial Step.send() could erase content saved by Step.update(), so step content persists when reloading threads.

  • Bug Fixes
    • Use NULLIF(EXCLUDED.input/output, '') in ON CONFLICT updates so COALESCE keeps existing non-empty values.
    • Add regression test to verify NULLIF is applied to both fields.

Written for commit 8095410. Summary will update on new commits.

When cl.Step initializes, input and output default to empty strings.
The create_step SQL uses COALESCE(EXCLUDED.output, "Step".output) on
conflict, but COALESCE treats empty string as a valid non-NULL value.
If the initial Step.send() (with empty output) commits to the database
after Step.update() (with actual content), the empty string overwrites
the real content.

Wrap EXCLUDED.output and EXCLUDED.input with NULLIF(..., '') so that
empty strings are treated as NULL by COALESCE, preserving any existing
non-empty content in the database.

Fixes #2789
@dosubot dosubot bot added size:S This PR changes 10-29 lines, ignoring generated files. bug Something isn't working data layer Pertains to data layers. unit-tests Has unit tests. labels Feb 28, 2026
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

@hayescode hayescode added this pull request to the merge queue Mar 5, 2026
Merged via the queue into Chainlit:main with commit c894d85 Mar 5, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working data layer Pertains to data layers. size:S This PR changes 10-29 lines, ignoring generated files. unit-tests Has unit tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Race condition in ChainlitDataLayer causes Steps to lose content in History

2 participants