Skip to content

Commit f7ab259

Browse files
committed
fix(session): Harden session copying
Add a dedicated copy_session API and preserve selected-turn fork requests. Require whole_session_copy_v1 before copying on a peer host. Disable copying empty sessions using persisted history counts. Preserve copy success when metadata refresh fails and fence device switches. Refresh capability projections and cover unsupported peers, host downgrades, and metadata failure paths. Refs: #3350
1 parent d1c7bf2 commit f7ab259

42 files changed

Lines changed: 450 additions & 126 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/interactive-capabilities/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ OpenBitFun Playbook currently contains **22 features**, **21 settings pages**, a
2727
- Generated per-item interaction audit: `docs/interactive-capabilities/technical/product-control-open-audit.json`
2828
- Generated low-level audit map: `docs/interactive-capabilities/technical/tauri-command-map.json`
2929

30-
说明书、网站、搜索和智能体只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **673** 个 Tauri 命令只用于实现覆盖审计。产品 UI 交互源码会在生成和检查时扫描并校验,但不会保存成随普通 UI 改动频繁变化的版本化快照。
30+
说明书、网站、搜索和智能体只看“功能 + 设置 + 子能力”。每项子能力都必须引用已注册 Tauri Command 或可解析的源码标记;这些证据不会进入公开目录。当前 **674** 个 Tauri 命令只用于实现覆盖审计。产品 UI 交互源码会在生成和检查时扫描并校验,但不会保存成随普通 UI 改动频繁变化的版本化快照。
3131

32-
Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **673** Tauri commands remain implementation-audit evidence only. Product UI interaction sources are scanned and validated during generation and checks, but are not stored as a versioned snapshot that churns with ordinary UI changes.
32+
Docs, website, search, and agents see only features, settings, and documented sub-capabilities. Every sub-capability must reference a registered Tauri command or a resolvable source marker; evidence is stripped from public projections. The **674** Tauri commands remain implementation-audit evidence only. Product UI interaction sources are scanned and validated during generation and checks, but are not stored as a versioned snapshot that churns with ordinary UI changes.
3333

3434
## 控制边界 / Control boundary
3535

‎docs/interactive-capabilities/capabilities.json‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"title": "OpenBitFun Playbook",
55
"origin": "https://playbook.openbitfun.com",
66
"source": "src/shared/interactive-capabilities/catalog.json",
7-
"digest": "09e162e571595bbe38f6c2e10e061e0f7243baae9e6db56a2e5b8948304a0e32",
7+
"digest": "dcea667ca017ed22790f12c28d5ccd4011314fca16d7e2ce9fc9cf87ff283825",
88
"ownerDigest": "c989ace53614c2a526fefe64816fdd6895ec71edcc28f44b6f80d1edb75bb33e",
99
"searchAcceptance": [
1010
{
@@ -19435,8 +19435,8 @@
1943519435
},
1943619436
{
1943719437
"id": "session-lifecycle",
19438-
"titleZh": "重命名、归档、恢复、删除、分叉与导出会话",
19439-
"titleEn": "Rename, archive, restore, delete, fork, and export sessions",
19438+
"titleZh": "重命名、归档、恢复、删除、复制、分叉与导出会话",
19439+
"titleEn": "Rename, archive, restore, delete, copy, fork, and export sessions",
1944019440
"control": {
1944119441
"kind": "open",
1944219442
"reasonCode": "visualSelection",
@@ -19735,8 +19735,8 @@
1973519735
"Safely customize the running OpenBitFun frontend in Creative mode",
1973619736
"打开并新建一个 AI 会话",
1973719737
"Open and create a new AI session",
19738-
"重命名、归档、恢复、删除、分叉与导出会话",
19739-
"Rename, archive, restore, delete, fork, and export sessions",
19738+
"重命名、归档、恢复、删除、复制、分叉与导出会话",
19739+
"Rename, archive, restore, delete, copy, fork, and export sessions",
1974019740
"为会话选择模型、工作模式、记忆模式与权限模式",
1974119741
"Choose the model, work mode, memory mode, and permission mode per session",
1974219742
"让创造模式扩展客户端界面与运行时能力",

‎docs/interactive-capabilities/capabilities/feature.ai-assistant.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ Work with AI in project context, create sessions, continue tasks, answer permiss
1919

2020
- **智能体可直接控制 / Direct Agent control** · 打开并新建一个 AI 会话
2121
- Open and create a new AI session
22-
- **智能体可定位入口,需交互完成 / Agent opens; interaction required** · 重命名、归档、恢复、删除、分叉与导出会话
23-
- Rename, archive, restore, delete, fork, and export sessions
22+
- **智能体可定位入口,需交互完成 / Agent opens; interaction required** · 重命名、归档、恢复、删除、复制、分叉与导出会话
23+
- Rename, archive, restore, delete, copy, fork, and export sessions
2424
- **智能体可定位入口,需交互完成 / Agent opens; interaction required** · 为会话选择模型、工作模式、记忆模式与权限模式
2525
- Choose the model, work mode, memory mode, and permission mode per session
2626
- **由专用智能体工具控制 / Delegated Agent tool** · `FrontendWorkbench` / `Read` / `Write` / `Edit` · 让创造模式扩展客户端界面与运行时能力

‎docs/interactive-capabilities/technical/product-control-open-audit.json‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"schemaVersion": 1,
33
"generatedFrom": "src/shared/interactive-capabilities/catalog.json",
4-
"catalogDigest": "09e162e571595bbe38f6c2e10e061e0f7243baae9e6db56a2e5b8948304a0e32",
4+
"catalogDigest": "dcea667ca017ed22790f12c28d5ccd4011314fca16d7e2ce9fc9cf87ff283825",
55
"count": 216,
66
"reasonCounts": {
77
"externalAuth": 4,
@@ -13,8 +13,8 @@
1313
{
1414
"capabilityId": "feature.ai-assistant",
1515
"itemId": "session-lifecycle",
16-
"titleZh": "重命名、归档、恢复、删除、分叉与导出会话",
17-
"titleEn": "Rename, archive, restore, delete, fork, and export sessions",
16+
"titleZh": "重命名、归档、恢复、删除、复制、分叉与导出会话",
17+
"titleEn": "Rename, archive, restore, delete, copy, fork, and export sessions",
1818
"reasonCode": "visualSelection",
1919
"reasonZh": "这些操作依赖选中的会话、当前运行状态或删除确认;智能体可准确打开会话界面,但不把新建会话冒充为完整生命周期控制。",
2020
"reasonEn": "These actions depend on the selected session, its live state, or deletion confirmation; the Agent can open the exact session UI without pretending that session creation controls the full lifecycle.",
@@ -27,6 +27,7 @@
2727
"command:archive_session",
2828
"command:restore_session",
2929
"command:delete_session",
30+
"command:copy_session",
3031
"command:fork_session",
3132
"command:export_session_transcript",
3233
"command:archive_all_sessions",

‎docs/interactive-capabilities/technical/tauri-command-map.json‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"schemaVersion": 2,
33
"generatedFrom": "src/shared/interactive-capabilities/catalog.json",
4-
"catalogDigest": "09e162e571595bbe38f6c2e10e061e0f7243baae9e6db56a2e5b8948304a0e32",
5-
"commandCount": 673,
4+
"catalogDigest": "dcea667ca017ed22790f12c28d5ccd4011314fca16d7e2ce9fc9cf87ff283825",
5+
"commandCount": 674,
66
"coverage": {
7-
"commandCount": 673,
8-
"documentedCommandCount": 625,
7+
"commandCount": 674,
8+
"documentedCommandCount": 626,
99
"implementationCommandCount": 48,
1010
"implementationDigest": "f6d38a24a70708988cb47ada81d07eccf0668684e8734c9ee5155b9ffa7e3db8"
1111
},
@@ -1644,6 +1644,22 @@
16441644
"signature": "fn control_deep_review_queue( request: ControlDeepReviewQueueRequest, ) -> Result<(), String>",
16451645
"remoteWorkspacePolicy": "LegacyUnaudited"
16461646
},
1647+
{
1648+
"id": "copy_session",
1649+
"moduleId": "session",
1650+
"capabilityId": "feature.ai-assistant",
1651+
"capabilityIds": [
1652+
"feature.ai-assistant"
1653+
],
1654+
"documentedItemIds": [
1655+
"feature.ai-assistant:session-lifecycle"
1656+
],
1657+
"visibility": "documented",
1658+
"rustPath": "copy_session",
1659+
"sourceFile": "src/apps/desktop/src/api/session_api.rs",
1660+
"signature": "fn copy_session( request: CopySessionRequest, runtime: State<'_, DesktopRuntimeContext>, ) -> Result<CopySessionResponse, String>",
1661+
"remoteWorkspacePolicy": "RemoteRouted"
1662+
},
16471663
{
16481664
"id": "create_acp_flow_session",
16491665
"moduleId": "acp_client",

‎src/apps/desktop/AGENTS.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ For session storage directory access, run
124124
for session storage directory resolution (these tests do not launch a file manager),
125125
and `cargo test -p openbitfun-desktop --lib remote_workspace_policy` after registration changes.
126126

127+
For whole-session copy routing, run
128+
`cargo test --locked -p openbitfun-desktop --lib whole_session_copy_uses_the_latest_persisted_turn_fork`.
129+
After command registration changes, also run the `remote_workspace_policy` filter.
130+
127131
For model selector request compatibility and configuration effect routing:
128132

129133
```bash

‎src/apps/desktop/src/api/session_api.rs‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,7 @@ fn default_include_hidden_subagents() -> bool {
240240
#[derive(Debug, Clone, Serialize, Deserialize)]
241241
pub struct ForkSessionRequest {
242242
pub source_session_id: String,
243-
/// Turn to copy the history through. Omitted means "copy the whole
244-
/// session": the host then forks at the session's latest persisted turn.
245-
///
246-
/// The field stays on the wire for existing callers and older frontends.
247-
#[serde(default, skip_serializing_if = "Option::is_none")]
248-
pub source_turn_id: Option<String>,
243+
pub source_turn_id: String,
249244
#[serde(default)]
250245
pub workspace_id: Option<String>,
251246
#[serde(default)]
@@ -258,6 +253,14 @@ pub struct ForkSessionRequest {
258253

259254
pub type ForkSessionResponse = SessionBranchResult;
260255

256+
#[derive(Debug, Clone, Serialize, Deserialize)]
257+
pub struct CopySessionRequest {
258+
pub source_session_id: String,
259+
pub workspace_id: String,
260+
}
261+
262+
pub type CopySessionResponse = SessionBranchResult;
263+
261264
#[derive(Debug, Clone, Serialize, Deserialize)]
262265
pub struct ArchiveSessionRequest {
263266
pub session_id: String,
@@ -728,8 +731,7 @@ pub async fn load_persisted_session_metadata(
728731
result
729732
}
730733

731-
/// Forks a session through a selected turn, or copies the whole session when
732-
/// `source_turn_id` is omitted.
734+
/// Forks a session through the caller's selected persisted turn.
733735
#[tauri::command]
734736
pub async fn fork_session(
735737
request: ForkSessionRequest,
@@ -751,6 +753,22 @@ pub async fn fork_session(
751753
.map_err(|error| format!("Failed to fork session: {}", desktop_session_error(error)))
752754
}
753755

756+
/// Copies the whole session through its latest persisted turn.
757+
#[tauri::command]
758+
pub async fn copy_session(
759+
request: CopySessionRequest,
760+
runtime: State<'_, DesktopRuntimeContext>,
761+
) -> Result<CopySessionResponse, String> {
762+
runtime
763+
.session_application()
764+
.copy_session(
765+
desktop_session_scope(Some(request.workspace_id), String::new(), None, None),
766+
request.source_session_id,
767+
)
768+
.await
769+
.map_err(|error| format!("Failed to copy session: {}", desktop_session_error(error)))
770+
}
771+
754772
#[tauri::command]
755773
pub async fn archive_session(
756774
request: ArchiveSessionRequest,

‎src/apps/desktop/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,6 +1618,7 @@ pub async fn run() {
16181618
touch_session_activity,
16191619
load_persisted_session_metadata,
16201620
fork_session,
1621+
copy_session,
16211622
archive_session,
16221623
unarchive_session,
16231624
archive_all_sessions,

‎src/apps/desktop/src/runtime/session_application.rs‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -621,30 +621,35 @@ impl DesktopSessionApplication {
621621
Ok(report)
622622
}
623623

624-
/// Copies a session into a new independent session.
625-
///
626-
/// `source_turn_id` selects a prefix of the source history. Omitting it
627-
/// copies the whole session, which is the runtime's latest-persisted-turn
628-
/// fork (`AgentSessionForkRequest`); the Desktop "Copy session" action uses
629-
/// that form so the controller never has to load a session's turns just to
630-
/// ask for a copy.
624+
/// Forks a session through the caller's selected persisted turn.
631625
pub(crate) async fn fork_session(
632626
&self,
633627
request: DesktopSessionScopeRequest,
634628
source_session_id: String,
635-
source_turn_id: Option<String>,
629+
source_turn_id: String,
636630
) -> DesktopSessionApplicationResult<SessionBranchResult> {
637631
let scope = self.resolved_scope(request).await?;
638632
self.ensure_runtime_ownership(&scope)?;
639633
fork_session_in_scope(
640634
&self.agent_runtime,
641635
scope,
642636
source_session_id,
643-
source_turn_id,
637+
Some(source_turn_id),
644638
)
645639
.await
646640
}
647641

642+
/// Copies the whole session using the existing runtime-owned fork algorithm.
643+
pub(crate) async fn copy_session(
644+
&self,
645+
request: DesktopSessionScopeRequest,
646+
source_session_id: String,
647+
) -> DesktopSessionApplicationResult<SessionBranchResult> {
648+
let scope = self.resolved_scope(request).await?;
649+
self.ensure_runtime_ownership(&scope)?;
650+
fork_session_in_scope(&self.agent_runtime, scope, source_session_id, None).await
651+
}
652+
648653
pub(crate) async fn set_session_archived(
649654
&self,
650655
request: DesktopSessionScopeRequest,

‎src/crates/contracts/product-domains/src/generated/product-control-catalog.json‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"title": "OpenBitFun Playbook",
55
"origin": "https://playbook.openbitfun.com",
66
"source": "src/shared/interactive-capabilities/catalog.json",
7-
"digest": "09e162e571595bbe38f6c2e10e061e0f7243baae9e6db56a2e5b8948304a0e32",
7+
"digest": "dcea667ca017ed22790f12c28d5ccd4011314fca16d7e2ce9fc9cf87ff283825",
88
"ownerDigest": "c989ace53614c2a526fefe64816fdd6895ec71edcc28f44b6f80d1edb75bb33e",
99
"searchAcceptance": [
1010
{
@@ -19435,8 +19435,8 @@
1943519435
},
1943619436
{
1943719437
"id": "session-lifecycle",
19438-
"titleZh": "重命名、归档、恢复、删除、分叉与导出会话",
19439-
"titleEn": "Rename, archive, restore, delete, fork, and export sessions",
19438+
"titleZh": "重命名、归档、恢复、删除、复制、分叉与导出会话",
19439+
"titleEn": "Rename, archive, restore, delete, copy, fork, and export sessions",
1944019440
"control": {
1944119441
"kind": "open",
1944219442
"reasonCode": "visualSelection",
@@ -19739,8 +19739,8 @@
1973919739
"Safely customize the running OpenBitFun frontend in Creative mode",
1974019740
"打开并新建一个 AI 会话",
1974119741
"Open and create a new AI session",
19742-
"重命名、归档、恢复、删除、分叉与导出会话",
19743-
"Rename, archive, restore, delete, fork, and export sessions",
19742+
"重命名、归档、恢复、删除、复制、分叉与导出会话",
19743+
"Rename, archive, restore, delete, copy, fork, and export sessions",
1974419744
"为会话选择模型、工作模式、记忆模式与权限模式",
1974519745
"Choose the model, work mode, memory mode, and permission mode per session",
1974619746
"让创造模式扩展客户端界面与运行时能力",

0 commit comments

Comments
 (0)