Skip to content

Commit 43eaca4

Browse files
committed
fix(miniapp): register agent workspaces before restoring floating chat
1 parent 1668097 commit 43eaca4

4 files changed

Lines changed: 163 additions & 17 deletions

File tree

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

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ use openbitfun_core::agentic::coordination::{
2626
use openbitfun_core::agentic::core::{MessageContent, MessageRole, Session, SessionConfig};
2727
use openbitfun_core::miniapp::agent_bridge::{
2828
agent_run_id_from_request, build_agent_submission_plan, extract_agent_turn_text,
29-
plan_agent_workspace, require_agent_prompt, require_enabled_agent_permissions,
30-
validate_reused_session, MiniAppAgentRateLimiter, MiniAppAgentRunRecord,
31-
MiniAppAgentRunRegistry, MiniAppAgentSubmissionPlan, MiniAppAgentTurnMessage,
32-
MiniAppAgentTurnMessageRole, MINIAPP_AGENT_KIND, UNKNOWN_AGENT_RUN_MESSAGE,
33-
UNKNOWN_AGENT_SESSION_MESSAGE,
29+
require_agent_prompt, require_enabled_agent_permissions, validate_reused_session,
30+
MiniAppAgentRateLimiter, MiniAppAgentRunRecord, MiniAppAgentRunRegistry,
31+
MiniAppAgentSubmissionPlan, MiniAppAgentTurnMessage, MiniAppAgentTurnMessageRole,
32+
MINIAPP_AGENT_KIND, UNKNOWN_AGENT_RUN_MESSAGE, UNKNOWN_AGENT_SESSION_MESSAGE,
3433
};
3534
use openbitfun_core::miniapp::agent_context::{
3635
remove_agent_context_snapshot, reserve_agent_context_snapshot, MiniAppAgentContextInput,
3736
MiniAppAgentContextSnapshot,
3837
};
38+
use openbitfun_core::miniapp::agent_workspace::prepare_agent_workspace;
3939
use openbitfun_core::OpenBitFunError;
4040

4141
// ============== Run registry ==============
@@ -393,15 +393,13 @@ pub async fn miniapp_agent_ensure_session(
393393
.miniapp_manager
394394
.path_manager()
395395
.miniapp_dir(&request.app_id);
396-
let workspace_plan = plan_agent_workspace(
396+
let workspace_plan = prepare_agent_workspace(
397+
&state.workspace_service,
397398
None,
398399
Some(request.app_data_workspace.as_str()),
399400
&app_data_dir,
400-
)?;
401-
if workspace_plan.create_if_missing {
402-
std::fs::create_dir_all(&workspace_plan.path)
403-
.map_err(|e| format!("Failed to create MiniApp agent workspace: {}", e))?;
404-
}
401+
)
402+
.await?;
405403

406404
let run_sequence = AGENT_RUN_COUNTER.fetch_add(1, Ordering::Relaxed);
407405
let run_id = agent_run_id_from_request(&request.app_id, None, run_sequence);
@@ -504,15 +502,13 @@ pub async fn miniapp_agent_run(
504502
.miniapp_manager
505503
.path_manager()
506504
.miniapp_dir(&request.app_id);
507-
let workspace_plan = plan_agent_workspace(
505+
let workspace_plan = prepare_agent_workspace(
506+
&state.workspace_service,
508507
request.workspace_path.as_deref(),
509508
request.app_data_workspace.as_deref(),
510509
&app_data_dir,
511-
)?;
512-
if workspace_plan.create_if_missing {
513-
std::fs::create_dir_all(&workspace_plan.path)
514-
.map_err(|e| format!("Failed to create MiniApp agent workspace: {}", e))?;
515-
}
510+
)
511+
.await?;
516512
let workspace_path = workspace_plan.workspace_path.clone();
517513
let run_sequence = if request
518514
.run_id

‎src/crates/assembly/core/AGENTS.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@ cargo test --locked -p openbitfun-core --no-default-features --features agent-ru
290290
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib list_workspaces
291291
```
292292

293+
MiniApp Agent appdata workspace registration and pre-ID session compatibility:
294+
295+
```bash
296+
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git,tools-miniapp --lib miniapp::agent_workspace::tests
297+
```
298+
293299
Skill hook activation, session cleanup, and tool preflight/permission ordering:
294300

295301
```bash
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//! Prepare host-owned MiniApp Agent workspaces before session creation/restore.
2+
3+
use std::path::Path;
4+
5+
use super::agent_bridge::{plan_agent_workspace, MiniAppAgentWorkspacePlan};
6+
use crate::service::workspace::WorkspaceService;
7+
8+
pub async fn prepare_agent_workspace(
9+
workspace_service: &WorkspaceService,
10+
explicit_workspace_path: Option<&str>,
11+
app_data_workspace: Option<&str>,
12+
app_data_dir: &Path,
13+
) -> Result<MiniAppAgentWorkspacePlan, String> {
14+
let plan = plan_agent_workspace(explicit_workspace_path, app_data_workspace, app_data_dir)?;
15+
if plan.create_if_missing {
16+
tokio::fs::create_dir_all(&plan.path)
17+
.await
18+
.map_err(|e| format!("Failed to create MiniApp agent workspace: {e}"))?;
19+
// Appdata topics predate workspace IDs and were never opened in the
20+
// workspace catalog. Register their host-owned directory before either
21+
// creating a session or restoring its legacy path-only config. Keep
22+
// the original session/history and the user's desktop selection.
23+
// Explicit workspace operands do not establish local ownership and
24+
// must continue through the existing workspace/remote admission path.
25+
workspace_service
26+
.register_local_workspace_record(plan.path.clone())
27+
.await
28+
.map_err(|e| format!("Failed to register MiniApp agent workspace: {e}"))?;
29+
}
30+
Ok(plan)
31+
}
32+
33+
#[cfg(test)]
34+
mod tests {
35+
use super::*;
36+
use crate::agentic::core::SessionConfig;
37+
use crate::service::workspace::legacy_compat::upgrade_session_workspace_reference;
38+
39+
#[tokio::test]
40+
async fn appdata_registration_upgrades_legacy_sessions_without_changing_selection() {
41+
let root = tempfile::tempdir().unwrap();
42+
let service = WorkspaceService::new_isolated_for_tests(root.path().join("user")).await;
43+
let project = root.path().join("project");
44+
tokio::fs::create_dir_all(&project).await.unwrap();
45+
let opened = service.open_workspace(project).await.unwrap();
46+
let appdata = root.path().join("miniapps/legacy-app");
47+
let chat = appdata.join("chat");
48+
tokio::fs::create_dir_all(&chat).await.unwrap();
49+
let chat = dunce::canonicalize(chat).unwrap();
50+
let legacy_payload = serde_json::json!({
51+
"max_context_tokens": 128000,
52+
"auto_compact": true,
53+
"enable_tools": true,
54+
"safe_mode": true,
55+
"max_turns": 100,
56+
"enable_context_compression": true,
57+
"workspace_path": chat,
58+
"model_id": "saved-model",
59+
});
60+
let mut config: SessionConfig = serde_json::from_value(legacy_payload.clone()).unwrap();
61+
let error = upgrade_session_workspace_reference(&mut config, &service)
62+
.await
63+
.unwrap_err();
64+
assert!(error
65+
.to_string()
66+
.contains("Legacy session workspace is unavailable"));
67+
68+
let plan = prepare_agent_workspace(&service, None, Some("chat"), &appdata)
69+
.await
70+
.unwrap();
71+
upgrade_session_workspace_reference(&mut config, &service)
72+
.await
73+
.unwrap();
74+
let id = config.workspace_id.clone().unwrap();
75+
assert_eq!(config.project_workspace_id.as_deref(), Some(id.as_str()));
76+
assert_eq!(config.workspace_path.as_deref(), chat.to_str());
77+
assert_eq!(config.model_id.as_deref(), Some("saved-model"));
78+
assert_eq!(dunce::canonicalize(plan.path).unwrap(), chat);
79+
80+
// Old readers still receive the original path and model; the additive
81+
// IDs survive a persisted-config round trip for upgraded readers.
82+
let saved = serde_json::to_value(&config).unwrap();
83+
assert_eq!(saved["workspace_path"], legacy_payload["workspace_path"]);
84+
assert_eq!(saved["model_id"], legacy_payload["model_id"]);
85+
let mut restored: SessionConfig = serde_json::from_value(saved).unwrap();
86+
prepare_agent_workspace(&service, None, Some("chat"), &appdata)
87+
.await
88+
.unwrap();
89+
upgrade_session_workspace_reference(&mut restored, &service)
90+
.await
91+
.unwrap();
92+
assert_eq!(restored.workspace_id.as_deref(), Some(id.as_str()));
93+
94+
assert_eq!(service.get_current_workspace().await.unwrap().id, opened.id);
95+
assert_eq!(service.get_opened_workspaces().await.len(), 1);
96+
assert!(service
97+
.get_recent_workspaces()
98+
.await
99+
.iter()
100+
.all(|w| w.id != id));
101+
}
102+
103+
#[tokio::test]
104+
async fn new_appdata_topics_are_registered_but_explicit_operands_are_not() {
105+
let root = tempfile::tempdir().unwrap();
106+
let service = WorkspaceService::new_isolated_for_tests(root.path().join("user")).await;
107+
let appdata = root.path().join("miniapps/app");
108+
let plan = prepare_agent_workspace(
109+
&service,
110+
Some("/remote/project"),
111+
Some("topics/new"),
112+
&appdata,
113+
)
114+
.await
115+
.unwrap();
116+
assert!(plan.path.is_dir());
117+
let records = service.list_workspace_infos().await;
118+
assert_eq!(records.len(), 1);
119+
assert_eq!(
120+
records[0].root_path,
121+
dunce::canonicalize(&plan.path).unwrap()
122+
);
123+
assert!(service.get_opened_workspaces().await.is_empty());
124+
assert!(service.get_recent_workspaces().await.is_empty());
125+
assert!(service.get_current_workspace().await.is_none());
126+
127+
// A remote POSIX operand (or an unknown local folder) must not be
128+
// registered locally just because a MiniApp supplied its path.
129+
let explicit = prepare_agent_workspace(&service, Some("/remote/project"), None, &appdata)
130+
.await
131+
.unwrap();
132+
assert!(!explicit.create_if_missing);
133+
assert_eq!(service.list_workspace_infos().await.len(), 1);
134+
assert!(
135+
prepare_agent_workspace(&service, None, Some("../outside"), &appdata)
136+
.await
137+
.is_err()
138+
);
139+
assert!(!appdata.join("../outside").exists());
140+
assert_eq!(service.list_workspace_infos().await.len(), 1);
141+
}
142+
}

‎src/crates/assembly/core/src/miniapp/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
33
#[cfg(feature = "agent-runtime")]
44
pub mod agent_context;
5+
#[cfg(feature = "agent-runtime")]
6+
pub mod agent_workspace;
57
pub mod builtin;
68
pub mod compiler;
79
pub mod exporter;

0 commit comments

Comments
 (0)