修复重进会话无法恢复运行中的 Direct 回合:Rust 增加活跃回合只读探测
新增 read_direct_codex_active_turn 只读命令:返回当前登记的 { clientTurnId, startedAt },供前端重进会话时接管正在跑的回合
DirectTaonierActiveInvocation 增加 started_at_ms 登记时刻,供只读探测与后续兜底释放判断这一轮的年龄
不改守卫互斥语义与 Drop 释放时机:探测既不进入也不抢占
把命令注册进 main.rs 的 invoke_handler
新增用例:登记中的回合能被只读探测看到、探测本身不占有不释放、Drop 之后回到 None
This commit is contained in:
@@ -263,12 +263,18 @@ fn direct_taonier_regeneration_invocation_sha256(invocation_id: &str) -> String
|
||||
#[derive(Debug)]
|
||||
struct DirectTaonierActiveInvocation {
|
||||
invocation_id: String,
|
||||
/// 登记时刻(Unix 毫秒)。只读探测与"终止"兜底都用它判断这一轮的年龄。
|
||||
started_at_ms: u64,
|
||||
}
|
||||
|
||||
static DIRECT_TAONIER_ACTIVE_INVOCATIONS: OnceLock<
|
||||
Mutex<HashMap<PathBuf, DirectTaonierActiveInvocation>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
fn direct_taonier_active_now_millis() -> u64 {
|
||||
unix_millis().min(u128::from(u64::MAX)) as u64
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DirectTaonierActiveInvocationGuard {
|
||||
root: PathBuf,
|
||||
@@ -300,6 +306,7 @@ impl DirectTaonierActiveInvocationGuard {
|
||||
root.clone(),
|
||||
DirectTaonierActiveInvocation {
|
||||
invocation_id: invocation_id.to_string(),
|
||||
started_at_ms: direct_taonier_active_now_millis(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -345,6 +352,46 @@ pub(crate) fn direct_taonier_active_invocation_id_at(root: &Path) -> Result<Stri
|
||||
})
|
||||
}
|
||||
|
||||
/// `read_direct_codex_active_turn` 的返回值:前端重进会话时用它恢复正在跑的回合。
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DirectActiveTurnView {
|
||||
/// 与事件 `turnId` 同一个身份(守卫里登记的 `invocation_id`)。
|
||||
pub(crate) client_turn_id: String,
|
||||
/// 这一轮登记的时刻(Unix 毫秒)。
|
||||
pub(crate) started_at: u64,
|
||||
}
|
||||
|
||||
/// 只读探测:该项目当前登记的 Direct 活跃回合(`None` = 没有回合在跑)。
|
||||
///
|
||||
/// 只读,不改变互斥语义与释放时机:守卫仍然只由回合自己的 `Drop` 释放。
|
||||
pub(crate) fn read_direct_taonier_active_invocation_at(
|
||||
root: &Path,
|
||||
) -> Result<Option<DirectActiveTurnView>, String> {
|
||||
let root = root
|
||||
.canonicalize()
|
||||
.map_err(|error| format!("无法锚定 Direct 调用项目目录:{error}"))?;
|
||||
Ok(DIRECT_TAONIER_ACTIVE_INVOCATIONS
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock()
|
||||
.map_err(|_| "Direct 调用身份锁已损坏".to_string())?
|
||||
.get(&root)
|
||||
.map(|active| DirectActiveTurnView {
|
||||
client_turn_id: active.invocation_id.clone(),
|
||||
started_at: active.started_at_ms,
|
||||
}))
|
||||
}
|
||||
|
||||
/// 只读命令:读取该项目当前登记的 Direct 活跃回合,供前端重进会话时恢复。
|
||||
///
|
||||
/// 不进入、不抢占、不释放守卫;没有回合时返回 `null`。
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_direct_codex_active_turn(
|
||||
project_path: String,
|
||||
) -> Result<Option<DirectActiveTurnView>, String> {
|
||||
read_direct_taonier_active_invocation_at(Path::new(project_path.trim()))
|
||||
}
|
||||
|
||||
fn direct_taonier_regeneration_project_id(root: &Path) -> Result<String, String> {
|
||||
let project_id = read_manifest(&root.join(".agent/manifest.json"))?
|
||||
.project_id
|
||||
@@ -4771,6 +4818,42 @@ mod tests {
|
||||
.expect("lost-response replay after the original turn finishes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_direct_active_turn_reports_the_registered_turn_and_disappears_after_drop() {
|
||||
let root = tempfile::tempdir().expect("active invocation root");
|
||||
assert_eq!(
|
||||
read_direct_taonier_active_invocation_at(root.path()).expect("read idle project"),
|
||||
None
|
||||
);
|
||||
|
||||
let first = DirectTaonierActiveInvocationGuard::enter(root.path(), "client-turn-read-1")
|
||||
.expect("first client turn");
|
||||
let running = read_direct_taonier_active_invocation_at(root.path())
|
||||
.expect("read running project")
|
||||
.expect("running turn is visible to the read-only probe");
|
||||
assert_eq!(running.client_turn_id, "client-turn-read-1");
|
||||
assert!(running.started_at > 0, "{running:?}");
|
||||
// camelCase 契约:前端按 `clientTurnId` / `startedAt` 取值。
|
||||
assert_eq!(
|
||||
serde_json::to_value(&running).expect("serialize view"),
|
||||
serde_json::json!({
|
||||
"clientTurnId": "client-turn-read-1",
|
||||
"startedAt": running.started_at,
|
||||
})
|
||||
);
|
||||
// 只读探测不占有、不释放:探测之后同项目第二次进入仍然被拒。
|
||||
let duplicate =
|
||||
DirectTaonierActiveInvocationGuard::enter(root.path(), "client-turn-read-2")
|
||||
.expect_err("read-only probe must not take over the project");
|
||||
assert!(!duplicate.starts_with(DIRECT_CODEX_TURN_ALREADY_RUNNING_PREFIX));
|
||||
|
||||
drop(first);
|
||||
assert_eq!(
|
||||
read_direct_taonier_active_invocation_at(root.path()).expect("read idle project"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_success_reply_is_persisted_once_with_the_stable_client_turn_identity() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
|
||||
@@ -2669,6 +2669,7 @@ fn main() {
|
||||
chat_with_game_creator_role_agent,
|
||||
chat_with_game_creator_role_agent_stream,
|
||||
chat_with_game_creator_direct_codex,
|
||||
read_direct_codex_active_turn,
|
||||
cancel_direct_codex_turn,
|
||||
select_game_creator_reasoning_effort,
|
||||
start_planning_session_v2,
|
||||
|
||||
Reference in New Issue
Block a user