mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 00:02:55 +00:00
* feat: run turns in several flow chat conversations at once Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep finished turns finished and cached chats current in the flow chat pool Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: attribute a turn's rows by job id as well as sequence Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: count only real stream updates and retry the job-id read Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep a chat that holds an unsent draft, and take one back when its first turn is withdrawn Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: ignore a stale running-turn snapshot, keep a withdrawn chat's draft, poll after clean stream ends Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: follow the turn running now when the listing named one already over Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep replacement turns and SSE fallback moving * fix: keep replacement turn handoffs active * fix: preserve unread badge line height * fix: settle local fallback handoffs * fix: settle refused turn handoffs * fix: scope turn handoffs to conversation * fix: drop stale turn handoffs * refactor: move the queued message and 409 handling into per-conversation turns Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address cubic's review of the parallel flow chat turns Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: clear a stale failure on refresh, and tighten the docs and test waits Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: recover running rows past the first page, and drop the failure a re-read disproves Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: check the running-turn query at compile time, and narrow what a refresh clears Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: settle a failed turn only from an answer that turn wrote Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: settle a failed turn from its own answer, and only while it is still the failure shown Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: drop a failure whose answer arrived even when a newer turn owns the error Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: free an answered failure whatever the turn that started meanwhile is doing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: drop a rows read that a turn outran, rather than merging it under newer messages Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: drop a rows read whose conversation was left and opened again Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: hand over a file still being read when its composer goes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: count a drop's routing as work in flight, so its file is handed over too Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: hold the send until every file a conversation is owed has landed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: keep a panel mounted per conversation instead of handing its draft over Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the withdrawn chat whose composer was written in, not the empty one Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the chat in front of the reader when both withdrawn composers were written in Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep a retry's own run arguments when a turn elsewhere refuses it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: name panels apart across pools, and read a flow's inputs when its chat is built Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
2.9 KiB
Rust
88 lines
2.9 KiB
Rust
//! A conversation answers one message at a time: while the run its newest user message started
|
|
//! is queued or running, the list reports that turn and a run into the conversation is refused.
|
|
//!
|
|
//! Uses runtime `sqlx::query` (not the compile-time macros) so no offline query cache is
|
|
//! needed, matching v2_job_delete_orphans.rs.
|
|
|
|
use sqlx::{Pool, Postgres};
|
|
use uuid::Uuid;
|
|
use windmill_common::error::Error;
|
|
use windmill_common::flow_conversations::{get_or_create_conversation_with_id, running_turns};
|
|
use windmill_test_utils::*;
|
|
|
|
const WS: &str = "test-workspace";
|
|
const CONV: Uuid = Uuid::from_u128(0x5eed);
|
|
|
|
/// Takes the conversation for a turn, as a run does, without keeping what it writes.
|
|
async fn take_conversation(db: &Pool<Postgres>) -> windmill_common::error::Result<Uuid> {
|
|
let mut tx = db.begin().await?;
|
|
let result =
|
|
get_or_create_conversation_with_id(&mut tx, WS, "f/flow", "test-user", "t", CONV, false)
|
|
.await;
|
|
tx.rollback().await?;
|
|
result.map(|c| c.id)
|
|
}
|
|
|
|
#[sqlx::test(fixtures("base"))]
|
|
async fn test_a_conversation_refuses_a_turn_while_one_runs(
|
|
db: Pool<Postgres>,
|
|
) -> anyhow::Result<()> {
|
|
initialize_tracing().await;
|
|
|
|
let job_id = Uuid::new_v4();
|
|
sqlx::query("INSERT INTO v2_job (id, workspace_id, kind) VALUES ($1, $2, 'flow')")
|
|
.bind(job_id)
|
|
.bind(WS)
|
|
.execute(&db)
|
|
.await?;
|
|
sqlx::query(
|
|
"INSERT INTO v2_job_queue (id, workspace_id, scheduled_for) VALUES ($1, $2, now())",
|
|
)
|
|
.bind(job_id)
|
|
.bind(WS)
|
|
.execute(&db)
|
|
.await?;
|
|
sqlx::query(
|
|
"INSERT INTO flow_conversation (id, workspace_id, flow_path, created_by)
|
|
VALUES ($1, $2, 'f/flow', 'test-user')",
|
|
)
|
|
.bind(CONV)
|
|
.bind(WS)
|
|
.execute(&db)
|
|
.await?;
|
|
let user_seq: i64 = sqlx::query_scalar(
|
|
"INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id)
|
|
VALUES ($1, 'user', 'hi', $2) RETURNING created_seq",
|
|
)
|
|
.bind(CONV)
|
|
.bind(job_id)
|
|
.fetch_one(&db)
|
|
.await?;
|
|
|
|
let running = running_turns(&db, &[CONV]).await?;
|
|
let turn = running
|
|
.get(&CONV)
|
|
.expect("the queued run is the conversation's running turn");
|
|
assert_eq!((turn.job_id, turn.user_seq), (job_id, user_seq));
|
|
|
|
match take_conversation(&db).await {
|
|
Err(Error::Generic(status, body)) => {
|
|
assert_eq!(status.as_u16(), 409);
|
|
assert!(
|
|
body.contains(&job_id.to_string()),
|
|
"the refusal names the running job: {body}"
|
|
);
|
|
}
|
|
other => panic!("expected a 409 while the first turn runs, got {other:?}"),
|
|
}
|
|
|
|
// The run ends: it leaves the queue, and the conversation takes the next message.
|
|
sqlx::query("DELETE FROM v2_job_queue WHERE id = $1")
|
|
.bind(job_id)
|
|
.execute(&db)
|
|
.await?;
|
|
assert!(running_turns(&db, &[CONV]).await?.is_empty());
|
|
assert_eq!(take_conversation(&db).await?, CONV);
|
|
Ok(())
|
|
}
|