mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 04:07:36 +08:00
refactor: update cursor request handling and improve parent request management
- Changed `run_id` to `request_id` in `CursorParent` struct for clarity. - Enhanced the `prepare` function to handle parent requests asynchronously, ensuring proper error handling for active runs. - Updated database interactions to include `cursor_request_id` for better tracking of requests. - Added tests to verify the behavior of reused cursor request IDs and their mapping to distinct executions.
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
-- Cursor may reuse one transport request id for multiple queued executions.
|
||||||
|
-- Keep that id as an association key while each local Run keeps its own identity.
|
||||||
|
ALTER TABLE runs ADD COLUMN cursor_request_id TEXT;
|
||||||
|
|
||||||
|
CREATE INDEX idx_runs_cursor_request_active
|
||||||
|
ON runs(cursor_request_id, status, created_at_ms DESC);
|
||||||
+35
-17
@@ -89,24 +89,42 @@ impl CursorActor {
|
|||||||
.map(|parent| parent.tool_call_id.clone()),
|
.map(|parent| parent.tool_call_id.clone()),
|
||||||
request.conversation_state.clone(),
|
request.conversation_state.clone(),
|
||||||
);
|
);
|
||||||
let parent = handle.parent().map(|parent| {
|
let prepared = async {
|
||||||
(
|
let parent = match handle.parent() {
|
||||||
crate::model::RunId::new(&parent.run_id),
|
Some(parent) => {
|
||||||
parent.tool_call_id.clone(),
|
let parent_run_id = dependencies
|
||||||
|
.store
|
||||||
|
.active_run_for_cursor_request(
|
||||||
|
&parent.request_id,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::Error::Protocol(format!(
|
||||||
|
"Cursor parent request {} has no active local Run",
|
||||||
|
parent.request_id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Some((
|
||||||
|
parent_run_id,
|
||||||
|
parent.tool_call_id.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
request::prepare(
|
||||||
|
handle.request_id(),
|
||||||
|
&request,
|
||||||
|
parent,
|
||||||
|
request::PrepareDependencies {
|
||||||
|
compiler: &dependencies.compiler,
|
||||||
|
store: &dependencies.store,
|
||||||
|
checkpoint: &checkpoint,
|
||||||
|
blob_sync: &blob_sync,
|
||||||
|
context_sync: &context_sync,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
});
|
.await
|
||||||
let prepared = request::prepare(
|
}
|
||||||
handle.request_id(),
|
|
||||||
&request,
|
|
||||||
parent,
|
|
||||||
request::PrepareDependencies {
|
|
||||||
compiler: &dependencies.compiler,
|
|
||||||
store: &dependencies.store,
|
|
||||||
checkpoint: &checkpoint,
|
|
||||||
blob_sync: &blob_sync,
|
|
||||||
context_sync: &context_sync,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
let (prepared, context) = match prepared {
|
let (prepared, context) = match prepared {
|
||||||
Ok(prepared) => prepared,
|
Ok(prepared) => prepared,
|
||||||
|
|||||||
@@ -203,12 +203,12 @@ async fn buffered(request: Request<Body>) -> Result<(axum::http::request::Parts,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parent_headers(headers: &HeaderMap) -> Result<Option<CursorParent>> {
|
fn parent_headers(headers: &HeaderMap) -> Result<Option<CursorParent>> {
|
||||||
let run_id = header_text(headers, "x-parent-request-id")?;
|
let request_id = header_text(headers, "x-parent-request-id")?;
|
||||||
let tool_call_id = header_text(headers, "x-parent-agent-tool-call-id")?;
|
let tool_call_id = header_text(headers, "x-parent-agent-tool-call-id")?;
|
||||||
match (run_id, tool_call_id) {
|
match (request_id, tool_call_id) {
|
||||||
(None, None) => Ok(None),
|
(None, None) => Ok(None),
|
||||||
(Some(run_id), Some(tool_call_id)) => Ok(Some(CursorParent {
|
(Some(request_id), Some(tool_call_id)) => Ok(Some(CursorParent {
|
||||||
run_id: run_id.into(),
|
request_id: request_id.into(),
|
||||||
tool_call_id: tool_call_id.into(),
|
tool_call_id: tool_call_id.into(),
|
||||||
})),
|
})),
|
||||||
_ => Err(crate::Error::Protocol(
|
_ => Err(crate::Error::Protocol(
|
||||||
@@ -270,7 +270,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
parent_headers(&headers).unwrap(),
|
parent_headers(&headers).unwrap(),
|
||||||
Some(CursorParent {
|
Some(CursorParent {
|
||||||
run_id: "parent-run".into(),
|
request_id: "parent-run".into(),
|
||||||
tool_call_id: "parent-call".into(),
|
tool_call_id: "parent-call".into(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cursor::prompting::{Mode, PromptCompiler},
|
cursor::prompting::{Mode, PromptCompiler},
|
||||||
cursor::{
|
cursor::{
|
||||||
@@ -71,9 +73,7 @@ pub(crate) async fn prepare(
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| request_id.into()),
|
.unwrap_or_else(|| request_id.into()),
|
||||||
);
|
);
|
||||||
// RunSSE/Bidi request_id identifies this concrete execution attempt. Cursor may
|
let run_id = execution_run_id(request_id);
|
||||||
// reuse AgentRunRequest.run_id when a queued or subagent-driven attempt resumes.
|
|
||||||
let run_id = RunId::new(request_id);
|
|
||||||
let mut base_messages = if request.conversation_state.is_some() {
|
let mut base_messages = if request.conversation_state.is_some() {
|
||||||
Some(
|
Some(
|
||||||
checkpoint
|
checkpoint
|
||||||
@@ -128,7 +128,7 @@ pub(crate) async fn prepare(
|
|||||||
starts_turn,
|
starts_turn,
|
||||||
compacting,
|
compacting,
|
||||||
background_completion,
|
background_completion,
|
||||||
} = action(request_id, request)?;
|
} = action(request)?;
|
||||||
let checkpoint_mode = if request.subagent_type_name.is_some() {
|
let checkpoint_mode = if request.subagent_type_name.is_some() {
|
||||||
Mode::Subagent
|
Mode::Subagent
|
||||||
} else {
|
} else {
|
||||||
@@ -286,6 +286,7 @@ pub(crate) async fn prepare(
|
|||||||
Ok((
|
Ok((
|
||||||
PreparedRun {
|
PreparedRun {
|
||||||
run_id,
|
run_id,
|
||||||
|
cursor_request_id: Some(request_id.into()),
|
||||||
conversation_id,
|
conversation_id,
|
||||||
kind,
|
kind,
|
||||||
model,
|
model,
|
||||||
@@ -348,12 +349,17 @@ fn validate_prompt_root(messages: &[CanonicalMessage]) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action(request_id: &str, request: &pb::AgentRunRequest) -> Result<ActionProjection> {
|
fn execution_run_id(request_id: &str) -> RunId {
|
||||||
let mode = request
|
let execution_id = Uuid::new_v4().simple().to_string();
|
||||||
|
RunId::new(format!("{request_id}:{}", &execution_id[..8]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(request: &pb::AgentRunRequest) -> Result<ActionProjection> {
|
||||||
|
let conversation_mode = request
|
||||||
.conversation_state
|
.conversation_state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|state| state.mode)
|
.and_then(|state| state.mode);
|
||||||
.unwrap_or(pb::AgentMode::Agent as i32);
|
let mode = conversation_mode.unwrap_or(pb::AgentMode::Agent as i32);
|
||||||
let Some(action) = request
|
let Some(action) = request
|
||||||
.action
|
.action
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -375,6 +381,11 @@ fn action(request_id: &str, request: &pb::AgentRunRequest) -> Result<ActionProje
|
|||||||
let user = action.user_message.as_ref().ok_or_else(|| {
|
let user = action.user_message.as_ref().ok_or_else(|| {
|
||||||
Error::Protocol("Cursor user message action has no UserMessage".into())
|
Error::Protocol("Cursor user message action has no UserMessage".into())
|
||||||
})?;
|
})?;
|
||||||
|
let mode = if user.mode == pb::AgentMode::Unspecified as i32 {
|
||||||
|
conversation_mode.unwrap_or(user.mode)
|
||||||
|
} else {
|
||||||
|
user.mode
|
||||||
|
};
|
||||||
if user.message_id.is_empty() {
|
if user.message_id.is_empty() {
|
||||||
return Err(Error::Protocol(
|
return Err(Error::Protocol(
|
||||||
"Cursor user message action has no message_id".into(),
|
"Cursor user message action has no message_id".into(),
|
||||||
@@ -382,7 +393,7 @@ fn action(request_id: &str, request: &pb::AgentRunRequest) -> Result<ActionProje
|
|||||||
}
|
}
|
||||||
if user.text.trim() == "/summarize" {
|
if user.text.trim() == "/summarize" {
|
||||||
return Ok(ActionProjection {
|
return Ok(ActionProjection {
|
||||||
mode: user.mode,
|
mode,
|
||||||
turn_user: Some(user.clone()),
|
turn_user: Some(user.clone()),
|
||||||
action_context: String::new(),
|
action_context: String::new(),
|
||||||
event_id: None,
|
event_id: None,
|
||||||
@@ -405,12 +416,13 @@ fn action(request_id: &str, request: &pb::AgentRunRequest) -> Result<ActionProje
|
|||||||
.filter(|text| !text.is_empty())
|
.filter(|text| !text.is_empty())
|
||||||
.cloned(),
|
.cloned(),
|
||||||
);
|
);
|
||||||
|
let event_id = format!("cursor:user:{}", user.message_id);
|
||||||
Ok(ActionProjection {
|
Ok(ActionProjection {
|
||||||
mode: user.mode,
|
mode,
|
||||||
turn_user: Some(user.clone()),
|
turn_user: Some(user.clone()),
|
||||||
action_context: context.join("\n\n"),
|
action_context: context.join("\n\n"),
|
||||||
event_id: Some(format!("run-request:{request_id}")),
|
event_id: Some(event_id.clone()),
|
||||||
input_id: Some(format!("cursor:user:{}", user.message_id)),
|
input_id: Some(event_id),
|
||||||
starts_turn: true,
|
starts_turn: true,
|
||||||
compacting: false,
|
compacting: false,
|
||||||
background_completion: false,
|
background_completion: false,
|
||||||
@@ -418,10 +430,11 @@ fn action(request_id: &str, request: &pb::AgentRunRequest) -> Result<ActionProje
|
|||||||
}
|
}
|
||||||
pb::conversation_action::Action::BackgroundTaskCompletionAction(action) => {
|
pb::conversation_action::Action::BackgroundTaskCompletionAction(action) => {
|
||||||
let projection = background::project(action, mode)?;
|
let projection = background::project(action, mode)?;
|
||||||
|
let event_id = projection.turn_user.message_id.clone();
|
||||||
Ok(ActionProjection {
|
Ok(ActionProjection {
|
||||||
mode,
|
mode,
|
||||||
action_context: projection.context,
|
action_context: projection.context,
|
||||||
event_id: Some(format!("run-request:{request_id}")),
|
event_id: Some(event_id),
|
||||||
input_id: None,
|
input_id: None,
|
||||||
turn_user: Some(projection.turn_user),
|
turn_user: Some(projection.turn_user),
|
||||||
starts_turn: true,
|
starts_turn: true,
|
||||||
@@ -598,6 +611,18 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn execution_run_id_keeps_the_request_id_and_adds_eight_uuid_hex_digits() {
|
||||||
|
let run_id = execution_run_id("01bba7c5-9c00-4922-b1df-1f58146b5d90");
|
||||||
|
let suffix = run_id
|
||||||
|
.as_str()
|
||||||
|
.strip_prefix("01bba7c5-9c00-4922-b1df-1f58146b5d90:")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(suffix.len(), 8);
|
||||||
|
assert!(suffix.bytes().all(|byte| byte.is_ascii_hexdigit()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn current_user_message_consumes_the_mode_instead_of_history_mode() {
|
fn current_user_message_consumes_the_mode_instead_of_history_mode() {
|
||||||
let request = pb::AgentRunRequest {
|
let request = pb::AgentRunRequest {
|
||||||
@@ -621,7 +646,7 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let projection = action("request", &request).unwrap();
|
let projection = action(&request).unwrap();
|
||||||
assert_eq!(projection.mode, pb::AgentMode::Ask as i32);
|
assert_eq!(projection.mode, pb::AgentMode::Ask as i32);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
projection.input_id.as_deref(),
|
projection.input_id.as_deref(),
|
||||||
@@ -630,6 +655,63 @@ mod tests {
|
|||||||
assert_eq!(mode_from_proto(projection.mode).unwrap(), Mode::Ask);
|
assert_eq!(mode_from_proto(projection.mode).unwrap(), Mode::Ask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn queued_user_message_without_mode_inherits_conversation_mode() {
|
||||||
|
let request = pb::AgentRunRequest {
|
||||||
|
conversation_state: Some(pb::ConversationStateStructure {
|
||||||
|
mode: Some(pb::AgentMode::Agent as i32),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
action: Some(pb::ConversationAction {
|
||||||
|
action: Some(pb::conversation_action::Action::UserMessageAction(
|
||||||
|
pb::UserMessageAction {
|
||||||
|
user_message: Some(pb::UserMessage {
|
||||||
|
text: "queued follow-up".into(),
|
||||||
|
message_id: "queued-user-message".into(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let projection = action(&request).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(projection.mode, pb::AgentMode::Agent as i32);
|
||||||
|
assert_eq!(mode_from_proto(projection.mode).unwrap(), Mode::Agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn queued_messages_reusing_a_request_id_keep_distinct_runtime_identities() {
|
||||||
|
let request = |message_id: &str| pb::AgentRunRequest {
|
||||||
|
action: Some(pb::ConversationAction {
|
||||||
|
action: Some(pb::conversation_action::Action::UserMessageAction(
|
||||||
|
pb::UserMessageAction {
|
||||||
|
user_message: Some(pb::UserMessage {
|
||||||
|
text: "queued follow-up".into(),
|
||||||
|
message_id: message_id.into(),
|
||||||
|
mode: pb::AgentMode::Agent as i32,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let first = action(&request("message-one")).unwrap();
|
||||||
|
let second = action(&request("message-two")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(first.event_id.as_deref(), Some("cursor:user:message-one"));
|
||||||
|
assert_eq!(second.event_id.as_deref(), Some("cursor:user:message-two"));
|
||||||
|
assert_ne!(first.event_id, second.event_id);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn execute_plan_appends_the_approved_plan_as_a_stable_runtime_event() {
|
fn execute_plan_appends_the_approved_plan_as_a_stable_runtime_event() {
|
||||||
let execute = pb::ExecutePlanAction {
|
let execute = pb::ExecutePlanAction {
|
||||||
@@ -648,8 +730,8 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let first = action("request-one", &request).unwrap();
|
let first = action(&request).unwrap();
|
||||||
let second = action("request-two", &request).unwrap();
|
let second = action(&request).unwrap();
|
||||||
assert_eq!(first.mode, pb::AgentMode::Agent as i32);
|
assert_eq!(first.mode, pb::AgentMode::Agent as i32);
|
||||||
assert!(first.starts_turn);
|
assert!(first.starts_turn);
|
||||||
assert_eq!(first.event_id, second.event_id);
|
assert_eq!(first.event_id, second.event_id);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ pub struct CursorSessionHandle {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct CursorParent {
|
pub struct CursorParent {
|
||||||
pub run_id: String,
|
pub request_id: String,
|
||||||
pub tool_call_id: String,
|
pub tool_call_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +69,9 @@ impl CursorSessionHandle {
|
|||||||
self.cancellation.clone()
|
self.cancellation.clone()
|
||||||
}
|
}
|
||||||
pub fn set_parent(&self, parent: CursorParent) -> Result<()> {
|
pub fn set_parent(&self, parent: CursorParent) -> Result<()> {
|
||||||
if parent.run_id.is_empty() || parent.tool_call_id.is_empty() {
|
if parent.request_id.is_empty() || parent.tool_call_id.is_empty() {
|
||||||
return Err(crate::Error::Protocol(
|
return Err(crate::Error::Protocol(
|
||||||
"Cursor parent run and tool call ids are required".into(),
|
"Cursor parent request and tool call ids are required".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if self.parent.get().is_some_and(|current| current != &parent) {
|
if self.parent.get().is_some_and(|current| current != &parent) {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ pub struct RecoveredToolRound {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct PreparedRun {
|
pub struct PreparedRun {
|
||||||
pub run_id: RunId,
|
pub run_id: RunId,
|
||||||
|
pub cursor_request_id: Option<String>,
|
||||||
pub conversation_id: ConversationId,
|
pub conversation_id: ConversationId,
|
||||||
pub kind: RunKind,
|
pub kind: RunKind,
|
||||||
pub model: ModelSpec,
|
pub model: ModelSpec,
|
||||||
|
|||||||
@@ -88,12 +88,13 @@ impl Store {
|
|||||||
run_kind_columns(&prepared.kind);
|
run_kind_columns(&prepared.kind);
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO runs
|
"INSERT INTO runs
|
||||||
(run_id, conversation_id, base_revision_id, head_revision_id,
|
(run_id, cursor_request_id, conversation_id, base_revision_id, head_revision_id,
|
||||||
parent_run_id, parent_tool_call_id, run_kind, subagent_kind,
|
parent_run_id, parent_tool_call_id, run_kind, subagent_kind,
|
||||||
status, created_at_ms, updated_at_ms)
|
status, created_at_ms, updated_at_ms)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?)",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(prepared.run_id.as_str())
|
.bind(prepared.run_id.as_str())
|
||||||
|
.bind(prepared.cursor_request_id.as_deref())
|
||||||
.bind(prepared.conversation_id.as_str())
|
.bind(prepared.conversation_id.as_str())
|
||||||
.bind(prepared.base_revision_id.0)
|
.bind(prepared.base_revision_id.0)
|
||||||
.bind(prepared.base_revision_id.0)
|
.bind(prepared.base_revision_id.0)
|
||||||
@@ -128,6 +129,22 @@ impl Store {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn active_run_for_cursor_request(
|
||||||
|
&self,
|
||||||
|
cursor_request_id: &str,
|
||||||
|
) -> Result<Option<RunId>> {
|
||||||
|
let run_id: Option<String> = sqlx::query_scalar(
|
||||||
|
"SELECT run_id FROM runs
|
||||||
|
WHERE cursor_request_id = ? AND status = 'running'
|
||||||
|
ORDER BY created_at_ms DESC
|
||||||
|
LIMIT 1",
|
||||||
|
)
|
||||||
|
.bind(cursor_request_id)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(run_id.map(RunId))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn begin_provider_call(&self, run_id: &RunId) -> Result<u64> {
|
pub async fn begin_provider_call(&self, run_id: &RunId) -> Result<u64> {
|
||||||
let index: Option<i64> = sqlx::query_scalar(
|
let index: Option<i64> = sqlx::query_scalar(
|
||||||
"UPDATE runs SET provider_call_index = provider_call_index + 1, updated_at_ms = ?
|
"UPDATE runs SET provider_call_index = provider_call_index + 1, updated_at_ms = ?
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ async fn background_subagent_completion_starts_a_simulated_parent_turn() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(messages.iter().any(|message| {
|
assert!(messages.iter().any(|message| {
|
||||||
message.runtime_event_id.as_deref() == Some("run-request:completion-request")
|
message.runtime_event_id.as_deref()
|
||||||
|
== Some("background-completed:BACKGROUND_TASK_KIND_SUBAGENT:child-id")
|
||||||
&& matches!(&message.content, MessageContent::Parts { parts } if !parts.is_empty())
|
&& matches!(&message.content, MessageContent::Parts { parts } if !parts.is_empty())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -130,8 +131,8 @@ async fn background_subagent_completion_starts_a_simulated_parent_turn() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
runtime_ids,
|
runtime_ids,
|
||||||
[
|
[
|
||||||
"runtime:run-request:completion-request",
|
"runtime:background-completed:BACKGROUND_TASK_KIND_SUBAGENT:child-id",
|
||||||
"runtime:run-request:completion-request-2"
|
"runtime:background-completed:BACKGROUND_TASK_KIND_SUBAGENT:child-id-2"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,8 +169,16 @@ async fn eligible_pending_checkpoint_resumes_tools_before_the_next_model_call()
|
|||||||
1,
|
1,
|
||||||
"resume must execute the pending batch before calling the model"
|
"resume must execute the pending batch before calling the model"
|
||||||
);
|
);
|
||||||
|
let resumed_run_id = store
|
||||||
|
.active_run_for_cursor_request("resumed-run")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
let resumed_round = store
|
let resumed_round = store
|
||||||
.tool_round(&ToolRoundId::new("resumed-run:round:resume"))
|
.tool_round(&ToolRoundId::new(format!(
|
||||||
|
"{}:round:resume",
|
||||||
|
resumed_run_id.as_str()
|
||||||
|
)))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ async fn prepared(store: &cursor_server::store::Store) -> PreparedRun {
|
|||||||
let root = store.ensure_conversation(&conversation_id).await.unwrap();
|
let root = store.ensure_conversation(&conversation_id).await.unwrap();
|
||||||
PreparedRun {
|
PreparedRun {
|
||||||
run_id: RunId::new("run"),
|
run_id: RunId::new("run"),
|
||||||
|
cursor_request_id: None,
|
||||||
conversation_id,
|
conversation_id,
|
||||||
kind: RunKind::Root,
|
kind: RunKind::Root,
|
||||||
model: ModelSpec::new("model"),
|
model: ModelSpec::new("model"),
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ async fn a_replaced_run_cannot_overwrite_its_cancelled_status() {
|
|||||||
let base_revision_id = store.ensure_conversation(&conversation_id).await.unwrap();
|
let base_revision_id = store.ensure_conversation(&conversation_id).await.unwrap();
|
||||||
let prepared = |run_id: &str| PreparedRun {
|
let prepared = |run_id: &str| PreparedRun {
|
||||||
run_id: RunId::new(run_id),
|
run_id: RunId::new(run_id),
|
||||||
|
cursor_request_id: None,
|
||||||
conversation_id: conversation_id.clone(),
|
conversation_id: conversation_id.clone(),
|
||||||
kind: RunKind::Root,
|
kind: RunKind::Root,
|
||||||
model: ModelSpec::new("model"),
|
model: ModelSpec::new("model"),
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ async fn call_summary_is_always_stored_and_payloads_follow_detailed_setting() {
|
|||||||
store
|
store
|
||||||
.claim_run(&PreparedRun {
|
.claim_run(&PreparedRun {
|
||||||
run_id: RunId::new("run-1"),
|
run_id: RunId::new("run-1"),
|
||||||
|
cursor_request_id: None,
|
||||||
conversation_id,
|
conversation_id,
|
||||||
kind: RunKind::Root,
|
kind: RunKind::Root,
|
||||||
model: ModelSpec::new(call.model_hash.clone()),
|
model: ModelSpec::new(call.model_hash.clone()),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ fn prepared(
|
|||||||
) -> PreparedRun {
|
) -> PreparedRun {
|
||||||
PreparedRun {
|
PreparedRun {
|
||||||
run_id: RunId::new(run_id),
|
run_id: RunId::new(run_id),
|
||||||
|
cursor_request_id: None,
|
||||||
conversation_id: conversation_id.clone(),
|
conversation_id: conversation_id.clone(),
|
||||||
kind: RunKind::Root,
|
kind: RunKind::Root,
|
||||||
model: ModelSpec::new("test-model"),
|
model: ModelSpec::new("test-model"),
|
||||||
@@ -89,6 +90,35 @@ async fn selecting_an_old_revision_creates_a_branch_without_old_suffixes() {
|
|||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reused_cursor_request_id_maps_to_the_current_distinct_execution() {
|
||||||
|
let (_directory, store) = fixtures::temp_store().await;
|
||||||
|
let conversation_id = ConversationId::new("queued-conversation");
|
||||||
|
let root = store.ensure_conversation(&conversation_id).await.unwrap();
|
||||||
|
|
||||||
|
let mut first = prepared("reused-request:11111111", &conversation_id, root);
|
||||||
|
first.cursor_request_id = Some("reused-request".into());
|
||||||
|
store.claim_run(&first).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.active_run_for_cursor_request("reused-request")
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
Some(first.run_id.clone())
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut second = prepared("reused-request:22222222", &conversation_id, root);
|
||||||
|
second.cursor_request_id = Some("reused-request".into());
|
||||||
|
store.claim_run(&second).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.active_run_for_cursor_request("reused-request")
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
Some(second.run_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn identical_runtime_event_is_exactly_once_and_conflicts_are_rejected() {
|
async fn identical_runtime_event_is_exactly_once_and_conflicts_are_rejected() {
|
||||||
let (_directory, store) = fixtures::temp_store().await;
|
let (_directory, store) = fixtures::temp_store().await;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ async fn unchanged_request_context_is_not_repeated_and_preserves_the_provider_pr
|
|||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request.history[1].message_id,
|
request.history[1].message_id,
|
||||||
"runtime:run-request:ask-request"
|
"runtime:cursor:user:wire-user"
|
||||||
);
|
);
|
||||||
assert!(!request.prompt.instructions.contains("workspace rule"));
|
assert!(!request.prompt.instructions.contains("workspace rule"));
|
||||||
assert!(!request.prompt.instructions.contains("<mcp_meta_tools>"));
|
assert!(!request.prompt.instructions.contains("<mcp_meta_tools>"));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ async fn runtime_event_is_appended_exactly_once() {
|
|||||||
let root = store.ensure_conversation(&conversation_id).await.unwrap();
|
let root = store.ensure_conversation(&conversation_id).await.unwrap();
|
||||||
let run = PreparedRun {
|
let run = PreparedRun {
|
||||||
run_id: RunId::new("run"),
|
run_id: RunId::new("run"),
|
||||||
|
cursor_request_id: None,
|
||||||
conversation_id: conversation_id.clone(),
|
conversation_id: conversation_id.clone(),
|
||||||
kind: RunKind::Root,
|
kind: RunKind::Root,
|
||||||
model: ModelSpec::new("model"),
|
model: ModelSpec::new("model"),
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use cursor_server::store::Store;
|
||||||
|
use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, Row};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn version_two_database_upgrades_with_cursor_request_mapping() {
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let database = directory.path().join("upgrade.db");
|
||||||
|
let pool = sqlx::SqlitePool::connect_with(
|
||||||
|
SqliteConnectOptions::new()
|
||||||
|
.filename(&database)
|
||||||
|
.create_if_missing(true),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let all = sqlx::migrate!("./migrations");
|
||||||
|
let prior = Migrator {
|
||||||
|
migrations: Cow::Owned(
|
||||||
|
all.iter()
|
||||||
|
.filter(|migration| migration.version <= 2)
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
ignore_missing: false,
|
||||||
|
locking: true,
|
||||||
|
no_tx: false,
|
||||||
|
};
|
||||||
|
prior.run(&pool).await.unwrap();
|
||||||
|
drop(pool);
|
||||||
|
|
||||||
|
let store = Store::connect(&format!("sqlite://{}", database.display()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let columns = sqlx::query("PRAGMA table_info(runs)")
|
||||||
|
.fetch_all(store.pool())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(columns
|
||||||
|
.iter()
|
||||||
|
.any(|column| column.get::<String, _>("name") == "cursor_request_id"));
|
||||||
|
}
|
||||||
@@ -89,7 +89,7 @@ async fn selected_image_bytes_flow_from_run_request_to_history_providers_and_che
|
|||||||
let user = requests[0]
|
let user = requests[0]
|
||||||
.history
|
.history
|
||||||
.iter()
|
.iter()
|
||||||
.find(|message| message.message_id == "runtime:run-request:image-run")
|
.find(|message| message.message_id == "runtime:cursor:user:image-user")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let ProjectedContent::Parts(parts) = &user.content else {
|
let ProjectedContent::Parts(parts) = &user.content else {
|
||||||
panic!("runtime user message must retain typed parts")
|
panic!("runtime user message must retain typed parts")
|
||||||
@@ -128,7 +128,7 @@ async fn selected_image_bytes_flow_from_run_request_to_history_providers_and_che
|
|||||||
let id = BlobId::from_bytes(raw_id).unwrap();
|
let id = BlobId::from_bytes(raw_id).unwrap();
|
||||||
let bytes = store.get_blob(&id).await.unwrap().unwrap();
|
let bytes = store.get_blob(&id).await.unwrap().unwrap();
|
||||||
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||||
if value["id"] == "runtime:run-request:image-run" {
|
if value["id"] == "runtime:cursor:user:image-user" {
|
||||||
user_root = Some(value);
|
user_root = Some(value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ async fn text_turn_runs_from_bidi_request_through_checkpoint_and_end_stream() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(messages[0].message_id.starts_with("request-context:"));
|
assert!(messages[0].message_id.starts_with("request-context:"));
|
||||||
assert_eq!(messages[0].role, Role::User);
|
assert_eq!(messages[0].role, Role::User);
|
||||||
assert_eq!(messages[1].message_id, "runtime:run-request:request");
|
assert_eq!(messages[1].message_id, "runtime:cursor:user:user");
|
||||||
assert_eq!(messages[1].role, Role::User);
|
assert_eq!(messages[1].role, Role::User);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
messages.len(),
|
messages.len(),
|
||||||
@@ -300,11 +300,14 @@ async fn text_turn_runs_from_bidi_request_through_checkpoint_and_end_stream() {
|
|||||||
.fetch_all(store.pool())
|
.fetch_all(store.pool())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(stored_runs.len(), 1);
|
||||||
stored_runs,
|
let execution_suffix = stored_runs[0]
|
||||||
vec!["request"],
|
.strip_prefix("request:")
|
||||||
"the concrete request_id, not Cursor's reusable wire run_id, owns the execution"
|
.expect("the local Run keeps the Cursor request id as a readable prefix");
|
||||||
);
|
assert_eq!(execution_suffix.len(), 8);
|
||||||
|
assert!(execution_suffix
|
||||||
|
.bytes()
|
||||||
|
.all(|byte| byte.is_ascii_hexdigit()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_run(conversation_id: &str, text: &str, model_id: &str) -> pb::AgentClientMessage {
|
fn client_run(conversation_id: &str, text: &str, model_id: &str) -> pb::AgentClientMessage {
|
||||||
|
|||||||
@@ -965,7 +965,7 @@ async fn provider_tool_use_waits_for_client_result_then_calls_provider_again() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let provider_call_index: i64 =
|
let provider_call_index: i64 =
|
||||||
sqlx::query_scalar("SELECT provider_call_index FROM runs WHERE run_id = ?")
|
sqlx::query_scalar("SELECT provider_call_index FROM runs WHERE cursor_request_id = ?")
|
||||||
.bind("tool-request")
|
.bind("tool-request")
|
||||||
.fetch_one(&database)
|
.fetch_one(&database)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ async fn results_commit_adjacent_pairs_in_arrival_order() {
|
|||||||
let root = store.ensure_conversation(&conversation_id).await.unwrap();
|
let root = store.ensure_conversation(&conversation_id).await.unwrap();
|
||||||
let run = PreparedRun {
|
let run = PreparedRun {
|
||||||
run_id: RunId::new("run"),
|
run_id: RunId::new("run"),
|
||||||
|
cursor_request_id: None,
|
||||||
conversation_id: conversation_id.clone(),
|
conversation_id: conversation_id.clone(),
|
||||||
kind: RunKind::Root,
|
kind: RunKind::Root,
|
||||||
model: ModelSpec::new("model"),
|
model: ModelSpec::new("model"),
|
||||||
|
|||||||
Reference in New Issue
Block a user