宿主:首页「运行中的项目」改由 Thread Manager 的逻辑回合导出
活动回合表的唯一事实源从"调用身份守卫"搬进逻辑回合占用,任务侧不再另建一张表。 - `ActiveDirectTurn` 带上快照字段(回合身份 / 项目名 / 起点 / 状态 / 活动 / 序号), 接单时初始化,收口时随占用一起消失 - 新增 `update_direct_thread_active_turn`(进度回填,只认身份一致且序号不倒退)与 `list_direct_active_turns`(只导出仍有未收口回合的 thread) - `DirectActiveTurnSnapshot` 移进 `direct_thread_manager`,`projectPath` 用线程身份, 与事件流里的项目身份是同一个字符串 - `DirectTaonierActiveInvocation` 退回纯单飞锁:只留调用身份与登记时刻 - 回合更新发射器不再按项目路径 canonicalize 找表,改为持线程身份回填 - `DirectTurnReservation::accept` 多带一个 `clientTurnId`(快照与进度匹配用), 与占用 token 是两个身份 - 上下文身份的两个测试补上"逻辑回合也接单"这一步:身份来自接单,不是调用守卫
This commit is contained in:
@@ -7791,6 +7791,7 @@ done
|
||||
// 这里补上同一步,于是这一轮的边界仍在同一个订阅里成对出现,历史里也有那条用户消息。
|
||||
let _reservation = crate::agent::direct_turn_accept::DirectTurnReservation::accept(
|
||||
&thread_id,
|
||||
"turn-0001",
|
||||
Some("direct-codex:turn-0001:user"),
|
||||
)
|
||||
.expect("accept logical turn");
|
||||
|
||||
@@ -435,8 +435,14 @@ mod tests {
|
||||
async fn a_replaced_active_turn_marks_the_batch_stale() {
|
||||
let (_temp, root) = project();
|
||||
std::fs::write(root.join("code.js"), "unchanged").unwrap();
|
||||
// 身份来自逻辑回合(Thread Manager):接单才是"这一轮在跑"的唯一登记。
|
||||
let owner = Arc::new(std::sync::Mutex::new(Some(
|
||||
DirectTaonierActiveInvocationGuard::enter(&root, "turn-before").unwrap(),
|
||||
DirectTurnReservation::accept(
|
||||
&direct_thread_id_for_project(&root),
|
||||
"turn-before",
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)));
|
||||
let swap = Arc::clone(&owner);
|
||||
let result = read_batch_with(
|
||||
@@ -447,7 +453,14 @@ mod tests {
|
||||
let result = read_file(r, f, b);
|
||||
let mut guard = swap.lock().unwrap();
|
||||
drop(guard.take());
|
||||
*guard = Some(DirectTaonierActiveInvocationGuard::enter(r, "turn-after").unwrap());
|
||||
*guard = Some(
|
||||
DirectTurnReservation::accept(
|
||||
&direct_thread_id_for_project(r),
|
||||
"turn-after",
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
result
|
||||
},
|
||||
)
|
||||
@@ -581,7 +594,14 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn host_prefetch_keeps_data_out_of_system_rules_and_matches_active_turn() {
|
||||
let (_temp, root) = project();
|
||||
// 调用身份(预取闸门)与逻辑回合(上下文身份)是两件事,生产入口两步都做。
|
||||
let _guard = DirectTaonierActiveInvocationGuard::enter(&root, "prefetch-turn").unwrap();
|
||||
let _turn = DirectTurnReservation::accept(
|
||||
&direct_thread_id_for_project(&root),
|
||||
"prefetch-turn",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let data = prefetch_turn_input(&root, "prefetch-turn")
|
||||
.await
|
||||
.unwrap()
|
||||
|
||||
@@ -472,25 +472,7 @@ fn direct_taonier_regeneration_invocation_sha256(invocation_id: &str) -> String
|
||||
#[derive(Debug)]
|
||||
struct DirectTaonierActiveInvocation {
|
||||
invocation_id: String,
|
||||
project_name: Option<String>,
|
||||
started_at: u64,
|
||||
status: String,
|
||||
activity: Option<String>,
|
||||
updated_at: u64,
|
||||
sequence: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DirectActiveTurnSnapshot {
|
||||
pub(crate) project_path: String,
|
||||
pub(crate) project_name: Option<String>,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) started_at: u64,
|
||||
pub(crate) status: String,
|
||||
pub(crate) activity: Option<String>,
|
||||
pub(crate) updated_at: u64,
|
||||
pub(crate) sequence: u64,
|
||||
}
|
||||
|
||||
static DIRECT_TAONIER_ACTIVE_INVOCATIONS: OnceLock<
|
||||
@@ -542,15 +524,7 @@ impl DirectTaonierActiveInvocationGuard {
|
||||
root.clone(),
|
||||
DirectTaonierActiveInvocation {
|
||||
invocation_id: invocation_id.to_string(),
|
||||
project_name: root
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(str::to_string),
|
||||
started_at,
|
||||
status: "accepted".to_string(),
|
||||
activity: Some("request-accepted".to_string()),
|
||||
updated_at: started_at,
|
||||
sequence: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -579,57 +553,6 @@ impl Drop for DirectTaonierActiveInvocationGuard {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn list_direct_active_turns() -> Result<Vec<DirectActiveTurnSnapshot>, String> {
|
||||
let active = DIRECT_TAONIER_ACTIVE_INVOCATIONS
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock()
|
||||
.map_err(|_| "Direct 调用身份锁已损坏".to_string())?;
|
||||
let mut turns = active
|
||||
.iter()
|
||||
.map(|(root, invocation)| DirectActiveTurnSnapshot {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
project_name: invocation.project_name.clone(),
|
||||
turn_id: invocation.invocation_id.clone(),
|
||||
started_at: invocation.started_at,
|
||||
status: invocation.status.clone(),
|
||||
activity: invocation.activity.clone(),
|
||||
updated_at: invocation.updated_at,
|
||||
sequence: invocation.sequence,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
turns.sort_by(|left, right| left.project_path.cmp(&right.project_path));
|
||||
Ok(turns)
|
||||
}
|
||||
|
||||
pub(crate) fn update_direct_active_turn(
|
||||
root: &Path,
|
||||
turn_id: &str,
|
||||
status: &str,
|
||||
activity: Option<&str>,
|
||||
sequence: u64,
|
||||
updated_at: u64,
|
||||
) {
|
||||
let Ok(root) = root.canonicalize() else {
|
||||
return;
|
||||
};
|
||||
let Some(active) = DIRECT_TAONIER_ACTIVE_INVOCATIONS.get() else {
|
||||
return;
|
||||
};
|
||||
let Ok(mut active) = active.lock() else {
|
||||
return;
|
||||
};
|
||||
let Some(invocation) = active.get_mut(&root) else {
|
||||
return;
|
||||
};
|
||||
if invocation.invocation_id != turn_id || sequence < invocation.sequence {
|
||||
return;
|
||||
}
|
||||
invocation.status = status.to_string();
|
||||
invocation.activity = activity.map(str::to_string);
|
||||
invocation.updated_at = updated_at;
|
||||
invocation.sequence = sequence;
|
||||
}
|
||||
|
||||
pub(crate) fn direct_taonier_active_invocation_id_at(root: &Path) -> Result<String, String> {
|
||||
let root = root
|
||||
.canonicalize()
|
||||
@@ -5972,36 +5895,6 @@ mod tests {
|
||||
entry.started_at = entry.started_at.saturating_sub(age_ms);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_turn_snapshot_tracks_progress_and_is_removed_after_drop() {
|
||||
let root = tempfile::tempdir().expect("active snapshot root");
|
||||
let turn_id = "client-turn-snapshot-0001";
|
||||
let guard = DirectTaonierActiveInvocationGuard::enter(root.path(), turn_id)
|
||||
.expect("active snapshot turn");
|
||||
update_direct_active_turn(
|
||||
root.path(),
|
||||
turn_id,
|
||||
"streaming",
|
||||
Some("response-finalization"),
|
||||
3,
|
||||
42,
|
||||
);
|
||||
let snapshot = list_direct_active_turns()
|
||||
.expect("list active turns")
|
||||
.into_iter()
|
||||
.find(|turn| turn.turn_id == turn_id)
|
||||
.expect("snapshot entry");
|
||||
assert_eq!(snapshot.status, "streaming");
|
||||
assert_eq!(snapshot.activity.as_deref(), Some("response-finalization"));
|
||||
assert_eq!(snapshot.sequence, 3);
|
||||
assert_eq!(snapshot.updated_at, 42);
|
||||
drop(guard);
|
||||
assert!(list_direct_active_turns()
|
||||
.expect("list after completion")
|
||||
.into_iter()
|
||||
.all(|turn| turn.turn_id != turn_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_success_reply_is_persisted_once_with_the_stable_client_turn_identity() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
|
||||
@@ -106,7 +106,7 @@ async fn chat_with_game_creator_direct_codex_typed(
|
||||
// ——开始事件发生在用户条目落盘之前,而落盘本身也可能失败。
|
||||
let thread_id = direct_thread_id_for_project(root);
|
||||
let user_item_id = direct_codex_user_item_id_for_client_turn_id(&turn_id);
|
||||
let reservation = DirectTurnReservation::accept(&thread_id, user_item_id.as_deref())?;
|
||||
let reservation = DirectTurnReservation::accept(&thread_id, &turn_id, user_item_id.as_deref())?;
|
||||
// 落盘即接单:接单成功就必须在历史里留下这条用户消息,哪怕这一轮随后失败。
|
||||
if let Err(error) = append_direct_project_user_message_at(root, &canonical_user_item) {
|
||||
let failure = DirectTurnError::EnvironmentNotReady {
|
||||
@@ -206,7 +206,7 @@ mod tests {
|
||||
let subscription = subscribe_direct_thread(&thread_id);
|
||||
let _ = consume_direct_thread(&subscription.subscription_id);
|
||||
let reservation =
|
||||
DirectTurnReservation::accept(&thread_id, Some("direct-codex:turn-1:user"))
|
||||
DirectTurnReservation::accept(&thread_id, "turn-1", Some("direct-codex:turn-1:user"))
|
||||
.expect("accept logical turn");
|
||||
let invocation =
|
||||
DirectTaonierActiveInvocationGuard::enter(&root, "turn-1").expect("enter invocation");
|
||||
|
||||
@@ -37,11 +37,42 @@ struct SubscriberState {
|
||||
|
||||
/// 一条正在跑的逻辑回合的占用:接单时登记,终态写出时解除。
|
||||
///
|
||||
/// `token` 是这一次接单的稳定身份:终态出口只有拿着同一个 token 的占用对象才能写兜底终态,
|
||||
/// 避免迟到的旧占用把新回合的边界顶掉。
|
||||
/// 它同时是首页「运行中的项目」快照的**唯一事实源**([`list_direct_active_turns`]):这一格的
|
||||
/// 生命周期就是"这一轮在不在跑",进度字段由运行时那一侧经 [`update_direct_thread_active_turn`]
|
||||
/// 回填。任务侧不再另建一张活动回合表——同一件事只许有一处真相。
|
||||
///
|
||||
/// 两个身份别混:
|
||||
/// - `token` 是这一次接单的占用身份:终态出口只有拿着同一个 token 的占用对象才能写兜底终态,
|
||||
/// 避免迟到的旧占用把新回合的边界顶掉。它不对外。
|
||||
/// - `turn_id` 是给界面看的回合身份(`clientTurnId` 派生),只服务快照与进度回填的匹配。
|
||||
#[derive(Clone, Debug)]
|
||||
struct ActiveDirectTurn {
|
||||
token: String,
|
||||
turn_id: String,
|
||||
project_name: Option<String>,
|
||||
started_at: u64,
|
||||
status: String,
|
||||
activity: Option<String>,
|
||||
updated_at: u64,
|
||||
sequence: u64,
|
||||
}
|
||||
|
||||
/// 首页「运行中的项目」的一条快照。
|
||||
///
|
||||
/// `project_path` 与线上其它地方的项目身份取同一个字符串:Thread Manager 的线程身份就是项目的
|
||||
/// canonical 路径(见 `direct_thread_id_for_project`),所以快照里的项目身份与事件流里的身份
|
||||
/// 永远能对上,不需要调用方再做一次归一。
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DirectActiveTurnSnapshot {
|
||||
pub(crate) project_path: String,
|
||||
pub(crate) project_name: Option<String>,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) started_at: u64,
|
||||
pub(crate) status: String,
|
||||
pub(crate) activity: Option<String>,
|
||||
pub(crate) updated_at: u64,
|
||||
pub(crate) sequence: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -175,6 +206,7 @@ impl DirectThreadManager {
|
||||
&mut self,
|
||||
thread_id: &str,
|
||||
token: &str,
|
||||
turn_id: &str,
|
||||
user_item_id: Option<&str>,
|
||||
started_at_ms: u64,
|
||||
) -> Result<DirectThreadEvent, String> {
|
||||
@@ -185,6 +217,17 @@ impl DirectThreadManager {
|
||||
}
|
||||
thread.active_turn = Some(ActiveDirectTurn {
|
||||
token: token.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
project_name: std::path::Path::new(thread_id)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(str::to_string),
|
||||
started_at: started_at_ms,
|
||||
// 与"还没有任何进度事件"的状态一致:运行时给出的第一条进度会覆盖它。
|
||||
status: "accepted".to_string(),
|
||||
activity: Some("request-accepted".to_string()),
|
||||
updated_at: started_at_ms,
|
||||
sequence: 0,
|
||||
});
|
||||
}
|
||||
Ok(self.append(
|
||||
@@ -193,6 +236,56 @@ impl DirectThreadManager {
|
||||
))
|
||||
}
|
||||
|
||||
/// 运行时回填这一轮的进度。只认"仍在跑 + 回合身份一致 + 序号不倒退"的那一次。
|
||||
///
|
||||
/// 返回是否真的写进去了:没有未收口的回合、身份对不上(上一轮迟到的进度)、序号倒退
|
||||
/// (乱序到达的旧进度)都必须原地丢弃,不能把快照改成过期的样子。
|
||||
fn update_active_turn(
|
||||
&mut self,
|
||||
thread_id: &str,
|
||||
turn_id: &str,
|
||||
status: &str,
|
||||
activity: Option<&str>,
|
||||
sequence: u64,
|
||||
updated_at: u64,
|
||||
) -> bool {
|
||||
let Some(active) = self
|
||||
.threads
|
||||
.get_mut(thread_id)
|
||||
.and_then(|thread| thread.active_turn.as_mut())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if active.turn_id != turn_id || sequence < active.sequence {
|
||||
return false;
|
||||
}
|
||||
active.status = status.to_string();
|
||||
active.activity = activity.map(str::to_string);
|
||||
active.updated_at = updated_at;
|
||||
active.sequence = sequence;
|
||||
true
|
||||
}
|
||||
|
||||
/// 首页快照:只导出仍有未收口逻辑回合的 thread。
|
||||
fn active_turn_snapshots(&self) -> Vec<DirectActiveTurnSnapshot> {
|
||||
self.threads
|
||||
.iter()
|
||||
.filter_map(|(thread_id, thread)| {
|
||||
let active = thread.active_turn.as_ref()?;
|
||||
Some(DirectActiveTurnSnapshot {
|
||||
project_path: thread_id.clone(),
|
||||
project_name: active.project_name.clone(),
|
||||
turn_id: active.turn_id.clone(),
|
||||
started_at: active.started_at,
|
||||
status: active.status.clone(),
|
||||
activity: active.activity.clone(),
|
||||
updated_at: active.updated_at,
|
||||
sequence: active.sequence,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 深层的终态出口:解除占用并写下 `turn.completed`。
|
||||
///
|
||||
/// 不校验 token:这一条由真正跑完这一轮的代码调用,终态就是它算出来的那个(CLI 这类没有
|
||||
@@ -472,6 +565,7 @@ pub(crate) fn append_direct_thread_event(
|
||||
pub(crate) fn accept_direct_thread_turn(
|
||||
thread_id: &str,
|
||||
token: &str,
|
||||
turn_id: &str,
|
||||
user_item_id: Option<&str>,
|
||||
started_at_ms: u64,
|
||||
) -> Result<(), String> {
|
||||
@@ -479,12 +573,37 @@ pub(crate) fn accept_direct_thread_turn(
|
||||
global_direct_thread_manager()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.accept_turn(thread_id, token, user_item_id, started_at_ms)?;
|
||||
.accept_turn(thread_id, token, turn_id, user_item_id, started_at_ms)?;
|
||||
}
|
||||
notify_direct_thread_subscribers(thread_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 运行时回填某一轮逻辑回合的进度(状态 / 活动 / 序号)。返回是否真的写进去了。
|
||||
pub(crate) fn update_direct_thread_active_turn(
|
||||
thread_id: &str,
|
||||
turn_id: &str,
|
||||
status: &str,
|
||||
activity: Option<&str>,
|
||||
sequence: u64,
|
||||
updated_at: u64,
|
||||
) -> bool {
|
||||
global_direct_thread_manager()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.update_active_turn(thread_id, turn_id, status, activity, sequence, updated_at)
|
||||
}
|
||||
|
||||
/// 首页「运行中的项目」快照:逻辑回合的唯一导出口(见 [`DirectActiveTurnSnapshot`])。
|
||||
pub(crate) fn list_direct_active_turns() -> Result<Vec<DirectActiveTurnSnapshot>, String> {
|
||||
let mut turns = global_direct_thread_manager()
|
||||
.lock()
|
||||
.map_err(|_| "Direct 线程管理器已损坏".to_string())?
|
||||
.active_turn_snapshots();
|
||||
turns.sort_by(|left, right| left.project_path.cmp(&right.project_path));
|
||||
Ok(turns)
|
||||
}
|
||||
|
||||
/// 深层终态出口:解除占用并写 `turn.completed`。
|
||||
pub(crate) fn complete_direct_thread_turn(thread_id: &str, event: DirectThreadEvent) {
|
||||
{
|
||||
@@ -890,4 +1009,63 @@ mod tests {
|
||||
"unfinished item at queue head blocks middle cleanup"
|
||||
);
|
||||
}
|
||||
|
||||
/// 首页快照就是逻辑回合的导出:接单即出现、进度按序号回填、收口即消失。
|
||||
fn snapshot_of(
|
||||
manager: &DirectThreadManager,
|
||||
thread_id: &str,
|
||||
) -> Option<DirectActiveTurnSnapshot> {
|
||||
manager
|
||||
.active_turn_snapshots()
|
||||
.into_iter()
|
||||
.find(|turn| turn.project_path == thread_id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_turn_snapshot_follows_the_logical_turn_lifecycle() {
|
||||
let mut manager = DirectThreadManager::with_limits(100, 100_000);
|
||||
let thread_id = "/tmp/快照项目";
|
||||
assert!(snapshot_of(&manager, thread_id).is_none());
|
||||
|
||||
manager
|
||||
.accept_turn(thread_id, "token-1", "turn-1", Some("u-1"), FIXED_AT_MS)
|
||||
.expect("accept");
|
||||
let accepted = snapshot_of(&manager, thread_id).expect("accepted turn is visible");
|
||||
assert_eq!(accepted.turn_id, "turn-1");
|
||||
assert_eq!(accepted.project_name.as_deref(), Some("快照项目"));
|
||||
assert_eq!(accepted.started_at, FIXED_AT_MS);
|
||||
assert_eq!(accepted.status, "accepted");
|
||||
assert_eq!(accepted.activity.as_deref(), Some("request-accepted"));
|
||||
assert_eq!(accepted.sequence, 0);
|
||||
|
||||
assert!(manager.update_active_turn(
|
||||
thread_id,
|
||||
"turn-1",
|
||||
"streaming",
|
||||
Some("file-write"),
|
||||
3,
|
||||
42,
|
||||
));
|
||||
let running = snapshot_of(&manager, thread_id).expect("running turn is visible");
|
||||
assert_eq!(running.status, "streaming");
|
||||
assert_eq!(running.activity.as_deref(), Some("file-write"));
|
||||
assert_eq!(running.sequence, 3);
|
||||
assert_eq!(running.updated_at, 42);
|
||||
|
||||
// 序号倒退与身份对不上的进度都不许改快照。
|
||||
assert!(!manager.update_active_turn(thread_id, "turn-1", "failed", None, 2, 99));
|
||||
assert!(!manager.update_active_turn(thread_id, "turn-2", "failed", None, 4, 99));
|
||||
assert_eq!(
|
||||
snapshot_of(&manager, thread_id)
|
||||
.expect("snapshot unchanged")
|
||||
.status,
|
||||
"streaming"
|
||||
);
|
||||
|
||||
manager.complete_turn(
|
||||
thread_id,
|
||||
DirectThreadEvent::turn_completed("completed".to_string(), 5_000),
|
||||
);
|
||||
assert!(snapshot_of(&manager, thread_id).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,16 +30,25 @@ impl DirectTurnReservation {
|
||||
/// 接单:登记占用并发出逻辑回合开始事件。
|
||||
///
|
||||
/// 失败表示这个 thread 已经有一条没收口的回合(并发接单),此时不改队列、不发事件。
|
||||
/// `client_turn_id` 是给界面看的回合身份(首页快照与进度回填按它匹配),与占用身份 `token`
|
||||
/// 是两件事:前者来自调用方,后者只活在这个进程里。
|
||||
pub(crate) fn accept(
|
||||
thread_id: &str,
|
||||
client_turn_id: &str,
|
||||
user_item_id: Option<&str>,
|
||||
) -> Result<Self, DirectTurnError> {
|
||||
let token = Uuid::new_v4().to_string();
|
||||
accept_direct_thread_turn(thread_id, &token, user_item_id, direct_tool_call_now_ms())
|
||||
.map_err(|existing| DirectTurnError::TurnAlreadyRunning {
|
||||
existing_invocation_id: existing,
|
||||
incoming_invocation_id: token.clone(),
|
||||
})?;
|
||||
accept_direct_thread_turn(
|
||||
thread_id,
|
||||
&token,
|
||||
client_turn_id,
|
||||
user_item_id,
|
||||
direct_tool_call_now_ms(),
|
||||
)
|
||||
.map_err(|existing| DirectTurnError::TurnAlreadyRunning {
|
||||
existing_invocation_id: existing,
|
||||
incoming_invocation_id: token.clone(),
|
||||
})?;
|
||||
Ok(Self {
|
||||
thread_id: thread_id.to_string(),
|
||||
token,
|
||||
@@ -108,7 +117,8 @@ mod tests {
|
||||
let thread = unique_thread("started");
|
||||
let subscription = watch(&thread);
|
||||
|
||||
let reservation = DirectTurnReservation::accept(&thread, Some("u-1")).expect("accept");
|
||||
let reservation =
|
||||
DirectTurnReservation::accept(&thread, "turn-1", Some("u-1")).expect("accept");
|
||||
|
||||
let events = pending(&subscription);
|
||||
assert_eq!(events.len(), 1, "{events:?}");
|
||||
@@ -126,11 +136,12 @@ mod tests {
|
||||
fn a_second_accept_is_rejected_while_the_turn_is_open() {
|
||||
let thread = unique_thread("busy");
|
||||
let subscription = watch(&thread);
|
||||
let reservation = DirectTurnReservation::accept(&thread, Some("u-1")).expect("accept");
|
||||
let reservation =
|
||||
DirectTurnReservation::accept(&thread, "turn-1", Some("u-1")).expect("accept");
|
||||
// 先取走第一条接单自己的开始事件,之后的"空"才只说明被拒的这一次没写东西。
|
||||
assert_eq!(pending(&subscription).len(), 1);
|
||||
|
||||
let rejected = DirectTurnReservation::accept(&thread, Some("u-2"));
|
||||
let rejected = DirectTurnReservation::accept(&thread, "turn-2", Some("u-2"));
|
||||
|
||||
assert!(matches!(
|
||||
rejected,
|
||||
@@ -145,7 +156,8 @@ mod tests {
|
||||
fn drop_without_a_terminal_writes_a_host_dropped_terminal() {
|
||||
let thread = unique_thread("drop");
|
||||
let subscription = watch(&thread);
|
||||
let reservation = DirectTurnReservation::accept(&thread, Some("u-1")).expect("accept");
|
||||
let reservation =
|
||||
DirectTurnReservation::accept(&thread, "turn-1", Some("u-1")).expect("accept");
|
||||
assert!(reservation.finish_if_unfinished(DirectTurnTerminal::host_dropped()));
|
||||
assert!(!direct_thread_turn_is_active(&thread));
|
||||
|
||||
@@ -172,7 +184,8 @@ mod tests {
|
||||
fn the_deep_terminal_wins_and_the_fallback_stays_silent() {
|
||||
let thread = unique_thread("deep");
|
||||
let subscription = watch(&thread);
|
||||
let reservation = DirectTurnReservation::accept(&thread, Some("u-1")).expect("accept");
|
||||
let reservation =
|
||||
DirectTurnReservation::accept(&thread, "turn-1", Some("u-1")).expect("accept");
|
||||
|
||||
// 深层收口:真正跑完这一轮的代码算出来的终态。
|
||||
let deep = DirectThreadEvent::turn_completed_failed(
|
||||
@@ -202,10 +215,11 @@ mod tests {
|
||||
#[test]
|
||||
fn the_thread_can_be_accepted_again_after_the_turn_is_settled() {
|
||||
let thread = unique_thread("again");
|
||||
let first = DirectTurnReservation::accept(&thread, Some("u-1")).expect("accept");
|
||||
let first = DirectTurnReservation::accept(&thread, "turn-1", Some("u-1")).expect("accept");
|
||||
drop(first);
|
||||
|
||||
let second = DirectTurnReservation::accept(&thread, Some("u-2")).expect("second accept");
|
||||
let second =
|
||||
DirectTurnReservation::accept(&thread, "turn-2", Some("u-2")).expect("second accept");
|
||||
|
||||
assert!(direct_thread_turn_is_active(&thread));
|
||||
drop(second);
|
||||
|
||||
@@ -32,6 +32,8 @@ pub(crate) fn emit_direct_game_creator_progress(root: &Path, stage: &str, messag
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DirectGameCreatorTurnUpdateEmitter {
|
||||
project_path: String,
|
||||
/// Thread Manager 的线程身份:进度只回填到"这一轮仍被占用"的那一格上。
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
sequence: Arc<AtomicU64>,
|
||||
}
|
||||
@@ -40,6 +42,7 @@ impl DirectGameCreatorTurnUpdateEmitter {
|
||||
pub(crate) fn new(root: &Path, turn_id: String) -> Self {
|
||||
Self {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
thread_id: crate::agent::direct_thread_id_for_project(root),
|
||||
turn_id,
|
||||
sequence: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
@@ -136,8 +139,8 @@ impl DirectGameCreatorTurnUpdateEmitter {
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.min(u64::MAX as u128) as u64;
|
||||
update_direct_active_turn(
|
||||
Path::new(&self.project_path),
|
||||
update_direct_thread_active_turn(
|
||||
&self.thread_id,
|
||||
&self.turn_id,
|
||||
status,
|
||||
activity,
|
||||
|
||||
Reference in New Issue
Block a user