From b48293fb1f6fee9703d4514d2bad8f68a47d4831 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:08:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E6=95=9B=E5=B9=B3=E5=8F=B0=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=20epoch=20=E9=87=8D=E8=A3=85=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E5=8E=9F=E7=94=9F=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E4=B8=8E=E6=B8=B2=E6=9F=93=E5=B1=82=E6=BC=82=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GUI owner 重装与清除改为按 authority 快照重定基准,保持原生计数与渲染层认知一致 - 注释写明 epoch 交接的授权前提,以及写入单调性由渲染层 reserve 与 durable session revision 保证 - 同主体换凭据的重装保持身份代次,在途 operation 的冻结会话继续有效 - 补充 epoch 重装与换主体失效的回归用例,替换原先错误假设原生计数器单调的用例 --- .../src-tauri/src/platform_session.rs | 79 ++++++------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/platform_session.rs b/apps/ai-game-creator-shell/src-tauri/src/platform_session.rs index 28be4dd3c..41fe89783 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/platform_session.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/platform_session.rs @@ -445,33 +445,14 @@ pub(crate) fn replace_platform_session_for_gui_owner( let mut current = platform_session() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); - // 新的 GUI authority epoch 可以替换任意旧会话,但两个计数器只能前进:把写入下限 - // 重置到请求值会让旧 epoch 的迟到写入重新变成“更新”。同一主体的重装(续期后的 - // Runner 重挂、同一 epoch 的重新对账)保持身份代次,只更新凭据。 - let same_identity = current.snapshot.as_ref().is_some_and(|current_snapshot| { - current_snapshot.user_id == snapshot.user_id - && current_snapshot.api_base_url == snapshot.api_base_url - }); - let next_identity_generation = if same_identity { - current - .identity_generation - .max(snapshot.identity_generation) - } else { - current - .identity_generation - .saturating_add(1) - .max(snapshot.identity_generation) - }; - let next_revision = current.revision.saturating_add(1).max(snapshot.revision); - current.revision = next_revision; - current.identity_generation = next_identity_generation; - current.snapshot = Some(PlatformSessionSnapshot { - user_id: snapshot.user_id, - access_token: snapshot.access_token, - api_base_url: snapshot.api_base_url, - identity_generation: next_identity_generation, - revision: next_revision, - }); + // 这条路径是 GUI authority epoch 的重定性入口:只有 durable claim 的 epoch + session + // revision 与登记完全一致时才会走到这里,新 epoch 可以替换旧进程留下的任意计数器。 + // 因此按调用方快照重定基准,让原生计数与渲染层认知严格一致;Runner 同 epoch 的幂等 + // 重挂仍走 install_platform_session_checked 的精确相等校验。写入的持续单调性由渲染层 + // reserve(max(本地 + 1, 原生下限 + 1))和 durable session revision 保证。 + current.revision = snapshot.revision; + current.identity_generation = snapshot.identity_generation; + current.snapshot = Some(snapshot); Ok(()) } @@ -544,12 +525,9 @@ pub(crate) fn clear_platform_session_for_gui_owner(identity_generation: u64, rev let mut current = platform_session() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); - // 与 replace 同一口径:清除也只能前进,不能让新 epoch 把下限归零。 - current.revision = current.revision.saturating_add(1).max(revision); - current.identity_generation = current - .identity_generation - .saturating_add(1) - .max(identity_generation); + // 与 replace 同一口径:epoch 交接按调用方快照重定基准,避免原生计数与渲染层认知漂移。 + current.revision = revision; + current.identity_generation = identity_generation; current.snapshot = None; } @@ -849,7 +827,7 @@ mod tests { } #[test] - fn gui_owner_replacement_keeps_counters_monotonic_and_same_subject_identity() { + fn gui_owner_replacement_rebases_to_the_authority_and_only_subject_change_fences() { let _session = clear_test_platform_session(); replace_platform_session_for_gui_owner("gui-owner-a", "token-a", TEST_ORIGIN, 5, 5) .expect("install gui owner A"); @@ -857,37 +835,30 @@ mod tests { assert_eq!(installed.identity_generation, 5); assert_eq!(installed.revision, 5); - // 同一主体只换凭据:身份代次保持,写入 revision 前进。 + // 同一主体只换凭据(续期后 Runner 重挂走的就是这条 replace 路径):身份代次保持、 + // 写入 revision 前进,在途 operation 的冻结会话仍然有效。 replace_platform_session_for_gui_owner("gui-owner-a", "token-a2", TEST_ORIGIN, 5, 6) .expect("refresh gui owner A credential"); let refreshed = current_platform_session().expect("gui owner A refreshed session"); assert_eq!(refreshed.identity_generation, 5); assert_eq!(refreshed.revision, 6); + validate_frozen_platform_session(&installed) + .expect("same-subject credential replacement keeps the frozen session valid"); - // 迟到的旧 epoch 写入不能把写入下限拉回去。 - replace_platform_session_for_gui_owner("gui-owner-a", "token-a", TEST_ORIGIN, 5, 4) - .expect("stale gui owner write"); - let after_stale = - current_platform_session().expect("gui owner A session after stale write"); - assert_eq!( - after_stale.identity_generation, - refreshed.identity_generation - ); - assert!(after_stale.revision > refreshed.revision); - - // 换主体必须推进身份代次,使旧身份的在途 operation 失效。 - replace_platform_session_for_gui_owner("gui-owner-b", "token-b", TEST_ORIGIN, 5, 5) + // 换主体必须推进身份代次,旧身份的在途 operation 失败关闭。 + replace_platform_session_for_gui_owner("gui-owner-b", "token-b", TEST_ORIGIN, 6, 7) .expect("switch gui owner"); let switched = current_platform_session().expect("gui owner B session"); assert_eq!(switched.user_id, "gui-owner-b"); - assert!(switched.identity_generation > after_stale.identity_generation); - assert!(validate_frozen_platform_session(&after_stale).is_err()); + assert_eq!(switched.identity_generation, 6); + assert!(validate_frozen_platform_session(&installed).is_err()); + assert!(validate_frozen_platform_session(&refreshed).is_err()); - // 清除同样只能前进,不能把下限归零。 - clear_platform_session_for_gui_owner(0, 0); + // epoch 交接后的清除同样按调用方快照重定基准,让原生计数与渲染层认知一致。 + clear_platform_session_for_gui_owner(7, 8); let cleared = current_platform_session_write_state(); - assert!(cleared.revision > switched.revision); - assert!(cleared.identity_generation > switched.identity_generation); + assert_eq!(cleared.revision, 8); + assert_eq!(cleared.identity_generation, 7); assert!(current_platform_session().is_none()); }