From 4ee585e07dbdf967f3363267dd5a40fa3758b641 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Tue, 15 Sep 2026 16:17:15 +0200 Subject: [PATCH] fix(chat): guard every write-back behind the re-point, and order the conversation locks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The streaming path had the same unguarded gap the settle path was fixed for: its final poll is awaited and then `endTurn(settled)` runs regardless. Settling releases the conversation's queue, so a message typed before a re-point was started against the flow now loaded, writing it into the previous flow's conversation and agent memory — the thing the forgetting exists to prevent. The poller and the sidebar's loader were unguarded too. Both are guarded at their own await now, which covers every caller rather than each call site. The editor chats keyed off `$initialPathStore || $pathStore`, which still falls back to the typed path on a flow that was never deployed — and there the summary field rewrites the path on every keystroke, so naming a new flow emptied the composer as you typed. They take the editor's stable identity instead, which is also what a preview run records. `FOR UPDATE` over several rows takes them in whatever order the plan plans, so two purges could acquire two conversations in opposite orders. Both sites order by id. The transaction the lock depends on is now stated on `delete_jobs`, which takes a bare connection and would silently lose the serialisation on one that autocommits. Co-Authored-By: Claude Opus 5 (1M context) --- ...239b8b0054b753cfbeb5dad1e8120fd5e8a672d50ef.json} | 4 ++-- ...a3774c7a30d71a5689efe72daf58bd1e8f6cf90c410.json} | 4 ++-- backend/windmill-api-jobs/src/jobs_export.rs | 2 +- backend/windmill-common/src/jobs.rs | 5 ++++- .../src/lib/components/FlowPreviewContent.svelte | 2 +- .../lib/components/flows/content/FlowInput.svelte | 2 +- .../flows/conversations/FlowChatManager.svelte.ts | 12 ++++++++++++ 7 files changed, 23 insertions(+), 8 deletions(-) rename backend/.sqlx/{query-72a67160f31a0e66e4487a4508a7955ed919ef4e455ea47efef171395b7fec96.json => query-4f52bf546579f26a1d22c239b8b0054b753cfbeb5dad1e8120fd5e8a672d50ef.json} (73%) rename backend/.sqlx/{query-0a5502ee13a1af720ca7cc0596a32f0fe91257bb31b595c05f64c7903ad80ead.json => query-ec295b3890a0018475ec0a3774c7a30d71a5689efe72daf58bd1e8f6cf90c410.json} (76%) diff --git a/backend/.sqlx/query-72a67160f31a0e66e4487a4508a7955ed919ef4e455ea47efef171395b7fec96.json b/backend/.sqlx/query-4f52bf546579f26a1d22c239b8b0054b753cfbeb5dad1e8120fd5e8a672d50ef.json similarity index 73% rename from backend/.sqlx/query-72a67160f31a0e66e4487a4508a7955ed919ef4e455ea47efef171395b7fec96.json rename to backend/.sqlx/query-4f52bf546579f26a1d22c239b8b0054b753cfbeb5dad1e8120fd5e8a672d50ef.json index 0f50aa0cab..a5695f75fd 100644 --- a/backend/.sqlx/query-72a67160f31a0e66e4487a4508a7955ed919ef4e455ea47efef171395b7fec96.json +++ b/backend/.sqlx/query-4f52bf546579f26a1d22c239b8b0054b753cfbeb5dad1e8120fd5e8a672d50ef.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id FROM flow_conversation WHERE id = ANY($1) AND workspace_id = $2 FOR UPDATE", + "query": "SELECT id FROM flow_conversation WHERE id = ANY($1) AND workspace_id = $2 ORDER BY id FOR UPDATE", "describe": { "columns": [ { @@ -19,5 +19,5 @@ false ] }, - "hash": "72a67160f31a0e66e4487a4508a7955ed919ef4e455ea47efef171395b7fec96" + "hash": "4f52bf546579f26a1d22c239b8b0054b753cfbeb5dad1e8120fd5e8a672d50ef" } diff --git a/backend/.sqlx/query-0a5502ee13a1af720ca7cc0596a32f0fe91257bb31b595c05f64c7903ad80ead.json b/backend/.sqlx/query-ec295b3890a0018475ec0a3774c7a30d71a5689efe72daf58bd1e8f6cf90c410.json similarity index 76% rename from backend/.sqlx/query-0a5502ee13a1af720ca7cc0596a32f0fe91257bb31b595c05f64c7903ad80ead.json rename to backend/.sqlx/query-ec295b3890a0018475ec0a3774c7a30d71a5689efe72daf58bd1e8f6cf90c410.json index 025cc5bc8f..1e35bd15ac 100644 --- a/backend/.sqlx/query-0a5502ee13a1af720ca7cc0596a32f0fe91257bb31b595c05f64c7903ad80ead.json +++ b/backend/.sqlx/query-ec295b3890a0018475ec0a3774c7a30d71a5689efe72daf58bd1e8f6cf90c410.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id FROM flow_conversation WHERE id = ANY($1) FOR UPDATE", + "query": "SELECT id FROM flow_conversation WHERE id = ANY($1) ORDER BY id FOR UPDATE", "describe": { "columns": [ { @@ -18,5 +18,5 @@ false ] }, - "hash": "0a5502ee13a1af720ca7cc0596a32f0fe91257bb31b595c05f64c7903ad80ead" + "hash": "ec295b3890a0018475ec0a3774c7a30d71a5689efe72daf58bd1e8f6cf90c410" } diff --git a/backend/windmill-api-jobs/src/jobs_export.rs b/backend/windmill-api-jobs/src/jobs_export.rs index 5fcee1a3dc..757c946edf 100644 --- a/backend/windmill-api-jobs/src/jobs_export.rs +++ b/backend/windmill-api-jobs/src/jobs_export.rs @@ -715,7 +715,7 @@ pub async fn delete_jobs( // delete one of a conversation's last messages, neither sees the other's uncommitted // delete, and the conversation and its memory are left with nothing to collect them. sqlx::query_scalar!( - "SELECT id FROM flow_conversation WHERE id = ANY($1) AND workspace_id = $2 FOR UPDATE", + "SELECT id FROM flow_conversation WHERE id = ANY($1) AND workspace_id = $2 ORDER BY id FOR UPDATE", &conversation_ids, &w_id ) diff --git a/backend/windmill-common/src/jobs.rs b/backend/windmill-common/src/jobs.rs index ad3bd31984..b8458dbc73 100644 --- a/backend/windmill-common/src/jobs.rs +++ b/backend/windmill-common/src/jobs.rs @@ -478,6 +478,9 @@ pub static WORKER_INTERNAL_SERVER_INLINE_UTILS: OnceCell error::Result<()> { sqlx::query!( "DELETE FROM dispatch_event WHERE producer_job_id = ANY($1)", @@ -504,7 +507,7 @@ pub async fn delete_jobs(conn: &mut sqlx::PgConnection, ids: &[uuid::Uuid]) -> e // neither would collect it and nothing would try again. Taking the conversation row // first serialises them: the second reads the first's delete and finds it empty. sqlx::query_scalar!( - "SELECT id FROM flow_conversation WHERE id = ANY($1) FOR UPDATE", + "SELECT id FROM flow_conversation WHERE id = ANY($1) ORDER BY id FOR UPDATE", &conversation_ids ) .fetch_all(&mut *conn) diff --git a/frontend/src/lib/components/FlowPreviewContent.svelte b/frontend/src/lib/components/FlowPreviewContent.svelte index 399149111e..0b2ab01d84 100644 --- a/frontend/src/lib/components/FlowPreviewContent.svelte +++ b/frontend/src/lib/components/FlowPreviewContent.svelte @@ -481,7 +481,7 @@ }} conversationKind="test" frame="boxed" - path={$initialPathStore || $pathStore} + path={$initialPathStore || fakeInitialPath} inputSchema={flowStore.val.schema} flowModules={flowStore.val.value?.modules} /> diff --git a/frontend/src/lib/components/flows/content/FlowInput.svelte b/frontend/src/lib/components/flows/content/FlowInput.svelte index 18922c9133..d41258ff7e 100644 --- a/frontend/src/lib/components/flows/content/FlowInput.svelte +++ b/frontend/src/lib/components/flows/content/FlowInput.svelte @@ -814,7 +814,7 @@