From 3e217adc14102218d27ccaa5a2b723bb85045dc5 Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 30 Jul 2026 22:11:40 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20profile=E8=A7=A3=E6=B1=BA=E3=81=A8model?= =?UTF-8?q?=20catalog=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/client/src/backend_runtime.rs | 2 - crates/client/src/ticket_role.rs | 27 +- crates/manifest/src/model_catalog.rs | 17 ++ crates/manifest/src/profile.rs | 55 ++-- crates/ticket/src/config.rs | 6 +- crates/tui/src/backend_worker_picker.rs | 1 - crates/tui/src/dashboard/tests.rs | 2 +- crates/tui/src/spawn.rs | 4 +- crates/worker-runtime/src/catalog.rs | 7 - crates/worker-runtime/src/config_bundle.rs | 2 - crates/worker-runtime/src/http_server.rs | 8 +- crates/worker-runtime/src/main.rs | 15 +- crates/worker-runtime/src/worker_backend.rs | 49 +--- crates/worker/src/spawn/tool.rs | 8 +- crates/workspace-server/src/hosts.rs | 178 ++++++------ .../workspace-server/src/profile_settings.rs | 26 +- crates/workspace-server/src/server.rs | 272 ++++++++++++------ ...26-07-30-spawn-worker-runtime-cli-drift.md | 29 ++ resources/models/builtin.toml | 11 + .../profiles/{default.dcdl => base.dcdl} | 6 +- resources/profiles/coder.dcdl | 2 +- resources/profiles/companion.dcdl | 2 +- resources/profiles/intake.dcdl | 2 +- resources/profiles/memory-consolidation.dcdl | 2 +- resources/profiles/orchestrator.dcdl | 2 +- resources/profiles/reviewer.dcdl | 2 +- .../sidebar/WorkersNavSection.svelte | 2 +- .../src/lib/workspace/sidebar/types.ts | 2 +- .../workspace/sidebar/worker-launch.test.ts | 5 +- .../lib/workspace/sidebar/worker-launch.ts | 8 +- .../src/lib/workspace/sidebar/workers.test.ts | 1 - .../workers/[workerId]/console/+page.svelte | 7 +- .../w/[workspaceId]/workers/+page.svelte | 2 +- .../w/[workspaceId]/workers/new/+page.svelte | 4 +- 34 files changed, 421 insertions(+), 347 deletions(-) create mode 100644 docs/report/2026-07-30-spawn-worker-runtime-cli-drift.md rename resources/profiles/{default.dcdl => base.dcdl} (86%) diff --git a/crates/client/src/backend_runtime.rs b/crates/client/src/backend_runtime.rs index bdd960d3..66f1b703 100644 --- a/crates/client/src/backend_runtime.rs +++ b/crates/client/src/backend_runtime.rs @@ -145,8 +145,6 @@ pub struct BackendWorkerSummary { pub display_name: String, pub label: String, #[serde(default)] - pub role: Option, - #[serde(default)] pub profile: Option, #[serde(default)] pub singleton_key: Option, diff --git a/crates/client/src/ticket_role.rs b/crates/client/src/ticket_role.rs index 6ef40e74..c31bca70 100644 --- a/crates/client/src/ticket_role.rs +++ b/crates/client/src/ticket_role.rs @@ -252,7 +252,7 @@ pub enum TicketRoleLaunchError { #[error(transparent)] LaunchConfig(#[from] TicketRoleLaunchConfigError), #[error( - "Ticket role `{role}` profile selector `{selector}` is not resolvable before launch: {message}. Configure `[ticket.roles.{role}].profile` with an executable concrete profile selector such as `builtin:default` or a project/user profile" + "Ticket role `{role}` profile selector `{selector}` is not resolvable before launch: {message}. Configure `[ticket.roles.{role}].profile` with an executable concrete profile selector such as `builtin:companion` or a project/user profile" )] ProfileResolution { role: TicketRole, @@ -701,17 +701,20 @@ mod tests { let mut config = String::from("[ticket]\n"); for role in roles { config.push_str(&format!( - "\n[ticket.roles.{role}]\nprofile = \"builtin:default\"\n" + "\n[ticket.roles.{role}]\nprofile = \"builtin:companion\"\n" )); } write_config(workspace, &config); } fn text_segment(plan: &TicketRoleLaunchPlan) -> &str { - match &plan.run_segments[1] { - Segment::Text { content } => content, - other => panic!("expected text segment, got {other:?}"), - } + plan.run_segments + .iter() + .find_map(|segment| match segment { + Segment::Text { content } => Some(content.as_str()), + _ => None, + }) + .expect("expected text segment") } async fn write_test_event(writer: &mut W, event: Event) @@ -949,7 +952,7 @@ profile = "project:no-such-ticket-role-profile" let plan = plan_ticket_role_launch(context).unwrap(); assert_eq!(plan.role, TicketRole::Intake); - assert_eq!(plan.profile, "builtin:default"); + assert_eq!(plan.profile, "builtin:companion"); } #[test] @@ -962,7 +965,7 @@ profile = "project:no-such-ticket-role-profile" language = "Japanese" [ticket.roles.intake] -profile = "builtin:default" +profile = "builtin:companion" "#, ); let context = TicketRoleLaunchContext::new(temp.path(), TicketRole::Intake); @@ -1024,7 +1027,7 @@ profile = "builtin:default" temp.path(), r#" [ticket.roles.reviewer] -profile = "builtin:default" +profile = "builtin:companion" launch_prompt = "$workspace/ticket/reviewer/launch" "#, ); @@ -1037,7 +1040,7 @@ launch_prompt = "$workspace/ticket/reviewer/launch" let text = text_segment(&plan); assert_eq!(plan.worker_name, "reviewer-fixed"); - assert_eq!(plan.profile, "builtin:default"); + assert_eq!(plan.profile, "builtin:companion"); assert_eq!( plan.launch_prompt_ref.as_deref(), Some("$workspace/ticket/reviewer/launch") @@ -1045,7 +1048,7 @@ launch_prompt = "$workspace/ticket/reviewer/launch" assert!(matches!(&plan.run_segments[0], Segment::Text { .. })); assert!(!text.contains("Configured launch_prompt")); assert!(!text.contains("$workspace/ticket/reviewer/launch")); - assert!(!text.contains("Profile selector: builtin:default")); + assert!(!text.contains("Profile selector: builtin:companion")); assert!(!text.contains("Role: reviewer")); assert!(!text.contains("system_instruction")); assert!(text.contains("Target Ticket:")); @@ -1056,7 +1059,7 @@ launch_prompt = "$workspace/ticket/reviewer/launch" .spawn_config(WorkerRuntimeCommand::for_executable("/bin/yoi")) .unwrap(); assert_eq!(spawn.worker_name, "reviewer-fixed"); - assert_eq!(spawn.profile.as_deref(), Some("builtin:default")); + assert_eq!(spawn.profile.as_deref(), Some("builtin:companion")); assert_eq!(spawn.workspace_root, temp.path()); assert!(spawn.cwd.is_none()); assert_eq!( diff --git a/crates/manifest/src/model_catalog.rs b/crates/manifest/src/model_catalog.rs index 0cb9c5e2..f88275c3 100644 --- a/crates/manifest/src/model_catalog.rs +++ b/crates/manifest/src/model_catalog.rs @@ -441,6 +441,23 @@ mod tests { assert_eq!(cfg.context_window, 123_456); } + #[test] + fn codex_gpt56_sol_catalog_clamps_public_window_to_backend_limit() { + let providers = load_builtin_providers().unwrap(); + let models = load_builtin_models().unwrap(); + let manifest = ModelManifest { + ref_: Some("codex-oauth/gpt-5.6-sol".into()), + ..Default::default() + }; + let cfg = resolve_with_catalogs(&manifest, &providers, &models).unwrap(); + assert_eq!(cfg.model_id, "gpt-5.6-sol"); + assert_eq!(cfg.context_window, 272_000); + assert_eq!(cfg.max_context_window, Some(272_000)); + let capability = cfg.capability.expect("catalog capability"); + assert!(capability.vision); + assert!(capability.reasoning.is_some()); + } + #[test] fn codex_gpt55_catalog_records_effective_context_window() { let providers = load_builtin_providers().unwrap(); diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs index 1be68f5b..f107a42a 100644 --- a/crates/manifest/src/profile.rs +++ b/crates/manifest/src/profile.rs @@ -21,7 +21,6 @@ use crate::{ }; const PROFILE_FORMAT_V1: &str = "yoi.profile.v1"; -const BUILTIN_DEFAULT_PROFILE_NAME: &str = "default"; const BUILTIN_MODEL_CATALOG: &str = include_str!("../../../resources/models/builtin.toml"); const WORKSPACE_OVERRIDE_LOCAL_FILENAME: &str = "override.local.toml"; @@ -32,11 +31,6 @@ struct BuiltinProfile { } const BUILTIN_PROFILES: &[BuiltinProfile] = &[ - BuiltinProfile { - name: BUILTIN_DEFAULT_PROFILE_NAME, - label: "builtin:default", - description: "Bundled default Yoi coding profile", - }, BuiltinProfile { name: "companion", label: "builtin:companion", @@ -300,23 +294,6 @@ impl ProfileRegistry { fn set_default(&mut self, default: ProfileDefault) { self.default = Some(default); } - fn set_builtin_default_if_available(&mut self) { - if self.default.is_some() { - return; - } - if self - .select_named( - Some(ProfileRegistrySource::Builtin), - BUILTIN_DEFAULT_PROFILE_NAME, - ) - .is_ok() - { - self.default = Some(ProfileDefault { - source: Some(ProfileRegistrySource::Builtin), - name: BUILTIN_DEFAULT_PROFILE_NAME.to_string(), - }); - } - } fn mark_default_flags(&mut self) { let Some(default) = self.default.clone() else { return; @@ -366,7 +343,6 @@ impl ProfileDiscovery { if let Some(path) = &self.project_config { load_profile_registry_file(&mut registry, ProfileRegistrySource::Project, path)?; } - registry.set_builtin_default_if_available(); registry.mark_default_flags(); Ok(registry) } @@ -886,9 +862,8 @@ fn read_profile_artifact_file(path: &Path) -> Result Option { - let mut value = builtin_default_profile_artifact(); + let mut value = builtin_base_profile_artifact(); match label { - "builtin:default" | "default" => Some(value), "builtin:companion" | "companion" => { apply_role_profile( &mut value, @@ -970,7 +945,7 @@ fn builtin_profile_artifact(label: &str) -> Option { } } -fn builtin_default_profile_artifact() -> serde_json::Value { +fn builtin_base_profile_artifact() -> serde_json::Value { serde_json::json!({ "slug": "default", "description": "Default Yoi coding profile.", @@ -1395,16 +1370,18 @@ mod tests { ); } #[test] - fn builtin_default_profile_is_registered_as_default() { + fn builtin_profiles_do_not_define_an_implicit_default() { let registry = ProfileDiscovery::with_sources(None, None) .discover() .unwrap(); - let default = registry.default_entry().unwrap(); - assert_eq!(default.source, ProfileRegistrySource::Builtin); - assert_eq!(default.name, BUILTIN_DEFAULT_PROFILE_NAME); - assert!(default.is_default); - assert_eq!(default.path, None); - assert_eq!(default.provenance, "builtin:default"); + assert!(matches!( + registry.default_entry(), + Err(ProfileError::NoDefaultProfile) + )); + assert!(matches!( + registry.select(&ProfileSelector::Default), + Err(ProfileError::NoDefaultProfile) + )); } #[test] fn builtin_role_profiles_are_registered_and_resolve() { @@ -1808,12 +1785,12 @@ worker_context_max_tokens = 68000 assert!(err.to_string().contains("model.auth.file")); } #[test] - fn builtin_default_resolves_without_external_evaluator() { + fn builtin_companion_resolves_without_external_evaluator() { let tmp = TempDir::new().unwrap(); let resolved = ProfileResolver::new() .with_workspace_base(tmp.path()) .resolve( - &ProfileSelector::source_named(ProfileRegistrySource::Builtin, "default"), + &ProfileSelector::source_named(ProfileRegistrySource::Builtin, "companion"), ProfileResolveOptions::with_worker_name("runtime-workspace"), ) .unwrap(); @@ -1829,15 +1806,15 @@ worker_context_max_tokens = 68000 assert!(!resolved.manifest.feature.ticket.orchestration_control); assert_eq!( resolved.profile.as_ref().unwrap().name.as_deref(), - Some("default") + Some("companion") ); assert_eq!( resolved.source, ProfileSource::Registry { source: ProfileRegistrySource::Builtin, - name: "default".into(), + name: "companion".into(), path: None, - provenance: Some("builtin:default".into()), + provenance: Some("builtin:companion".into()), } ); } diff --git a/crates/ticket/src/config.rs b/crates/ticket/src/config.rs index 43b005e0..062cce59 100644 --- a/crates/ticket/src/config.rs +++ b/crates/ticket/src/config.rs @@ -1192,7 +1192,7 @@ root = ".yoi/tickets" temp.path(), r#" [roles.intake] -profile = "builtin:default" +profile = "builtin:companion" "#, ); @@ -1203,7 +1203,7 @@ profile = "builtin:default" .unwrap() .profile .as_str(), - "builtin:default" + "builtin:companion" ); assert_eq!( config @@ -1313,7 +1313,7 @@ profile = "inherit" temp.path(), r#" [roles.investigator] -profile = "builtin:default" +profile = "builtin:companion" "#, ); diff --git a/crates/tui/src/backend_worker_picker.rs b/crates/tui/src/backend_worker_picker.rs index e8588958..4e24968d 100644 --- a/crates/tui/src/backend_worker_picker.rs +++ b/crates/tui/src/backend_worker_picker.rs @@ -363,7 +363,6 @@ mod tests { display_name: "label".to_string(), singleton_key: None, tags: Vec::new(), - role: None, profile: profile.map(str::to_string), workspace: BackendWorkerWorkspaceSummary { visibility: "workspace".to_string(), diff --git a/crates/tui/src/dashboard/tests.rs b/crates/tui/src/dashboard/tests.rs index 7bd677c7..c18ed9ec 100644 --- a/crates/tui/src/dashboard/tests.rs +++ b/crates/tui/src/dashboard/tests.rs @@ -2782,7 +2782,7 @@ fn dashboard_ticket_intake_finish_success_clears_composer_and_reports_pod() { implementation_worktree_root: PathBuf::from("/tmp/workspace/.worktree"), role: TicketRole::Intake, worker_name: "intake-worker".to_string(), - profile: "builtin:default".to_string(), + profile: "builtin:companion".to_string(), launch_prompt_ref: None, run_segments: vec![], }, diff --git a/crates/tui/src/spawn.rs b/crates/tui/src/spawn.rs index 3eb7d683..91e34a1b 100644 --- a/crates/tui/src/spawn.rs +++ b/crates/tui/src/spawn.rs @@ -699,10 +699,10 @@ description = "Project coder" .unwrap(); let (choices, default_index) = profile_choices_for_cwd(&project); - assert_eq!(choices[0].selector.as_deref(), Some("builtin:default")); + assert_eq!(choices[0].selector.as_deref(), Some("builtin:companion")); assert_eq!( choices[0].label, - "builtin:default — Bundled default Yoi coding profile" + "builtin:companion — Bundled Companion role profile" ); let project_index = choices .iter() diff --git a/crates/worker-runtime/src/catalog.rs b/crates/worker-runtime/src/catalog.rs index 3ba2a272..20ddab2d 100644 --- a/crates/worker-runtime/src/catalog.rs +++ b/crates/worker-runtime/src/catalog.rs @@ -8,17 +8,10 @@ use std::path::PathBuf; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", content = "value", rename_all = "snake_case")] pub enum ProfileSelector { - RuntimeDefault, Builtin(String), Named(String), } -impl Default for ProfileSelector { - fn default() -> Self { - Self::RuntimeDefault - } -} - /// Runtime fetch/caching metadata for a Backend-authored Decodal profile source archive. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ProfileSourceArchiveHttpRef { diff --git a/crates/worker-runtime/src/config_bundle.rs b/crates/worker-runtime/src/config_bundle.rs index a098bca7..12a440be 100644 --- a/crates/worker-runtime/src/config_bundle.rs +++ b/crates/worker-runtime/src/config_bundle.rs @@ -353,7 +353,6 @@ pub(crate) fn validate_profile_selector( bundle_id: Option<&str>, ) -> Result<(), RuntimeError> { match selector { - ProfileSelector::RuntimeDefault => Ok(()), ProfileSelector::Builtin(value) | ProfileSelector::Named(value) => { if value.trim().is_empty() { Err(RuntimeError::InvalidProfileSelector { @@ -544,7 +543,6 @@ fn declaration_sort_key(declaration: &ConfigDeclaration) -> String { fn profile_sort_key(selector: &ProfileSelector) -> String { match selector { - ProfileSelector::RuntimeDefault => "runtime_default".to_string(), ProfileSelector::Builtin(value) => format!("builtin\0{value}"), ProfileSelector::Named(value) => format!("named\0{value}"), } diff --git a/crates/worker-runtime/src/http_server.rs b/crates/worker-runtime/src/http_server.rs index fdb68012..1565bf90 100644 --- a/crates/worker-runtime/src/http_server.rs +++ b/crates/worker-runtime/src/http_server.rs @@ -1808,9 +1808,9 @@ mod ws_tests { } fn ws_create_request() -> CreateWorkerRequest { - let bundle = ws_test_bundle(ProfileSelector::RuntimeDefault); + let bundle = ws_test_bundle(ProfileSelector::Builtin("builtin:companion".to_string())); CreateWorkerRequest { - profile: ProfileSelector::RuntimeDefault, + profile: ProfileSelector::Builtin("builtin:companion".to_string()), display_name: None, profile_source: crate::catalog::ProfileSourceArchiveSource::Http { location: crate::catalog::ProfileSourceArchiveHttpRef { @@ -1845,7 +1845,9 @@ mod ws_tests { Runtime::with_execution_backend(RuntimeOptions::default(), Arc::new(WsBackend)) .unwrap(); runtime - .store_config_bundle(ws_test_bundle(ProfileSelector::RuntimeDefault)) + .store_config_bundle(ws_test_bundle(ProfileSelector::Builtin( + "builtin:companion".to_string(), + ))) .unwrap(); let worker = runtime .create_worker_scoped( diff --git a/crates/worker-runtime/src/main.rs b/crates/worker-runtime/src/main.rs index ca9cd7a1..19d942c4 100644 --- a/crates/worker-runtime/src/main.rs +++ b/crates/worker-runtime/src/main.rs @@ -91,9 +91,6 @@ fn build_runtime(config: &ProcessConfig) -> Result { ), )); } - if let Some(profile) = config.profile.clone() { - factory = factory.with_profile(profile); - } let backend = Arc::new( WorkerRuntimeExecutionBackend::new(factory) .map_err(ProcessError::WorkerAdapter)? @@ -171,9 +168,6 @@ where "--backend-resource-token" => { config.backend_resource_token = Some(take_value(&flag, inline_value, &mut args)?); } - "--profile" => { - config.profile = Some(take_value(&flag, inline_value, &mut args)?); - } "--store" => { let value = take_value(&flag, inline_value, &mut args)?; if !matches!(value.as_str(), "fs" | "fs-store") { @@ -286,7 +280,6 @@ struct ProcessConfig { no_store: bool, backend_resource_endpoint: Option, backend_resource_token: Option, - profile: Option, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -308,7 +301,6 @@ impl ProcessConfig { no_store: false, backend_resource_endpoint: None, backend_resource_token: None, - profile: None, }) } @@ -804,7 +796,6 @@ Browsers must not connect to this Runtime process directly. Options: --bind Bind socket address (default: 127.0.0.1:38800) --display-name Runtime display name - --profile Force spawned Workers to use a Profile selector --fs-root Filesystem-backed storage root (default: user data dir) --fs-worker-dir Worker storage directory (default: /worker) --fs-runtime-dir Runtime catalog directory (default: /runtime) @@ -852,6 +843,7 @@ mod tests { assert!(parse_args(["--worker-store-dir", "/tmp/sessions"]).is_err()); assert!(parse_args(["--worker-metadata-dir", "/tmp/metadata"]).is_err()); assert!(parse_args(["--worker-runtime-base-dir", "/tmp/runtime"]).is_err()); + assert!(parse_args(["--profile", "builtin:coder"]).is_err()); } #[test] @@ -866,8 +858,6 @@ mod tests { "127.0.0.1:0", "--display-name", "Runtime Alpha", - "--profile", - "builtin:coder", "--local-token-env", "TEST_RUNTIME_TOKEN", ]); @@ -881,8 +871,6 @@ mod tests { "127.0.0.1:0", "--display-name", "Runtime Alpha", - "--profile", - "builtin:coder", "--local-token-env", "TEST_RUNTIME_TOKEN", ]) @@ -894,7 +882,6 @@ mod tests { assert_eq!(config.http.bind_addr, "127.0.0.1:0".parse().unwrap()); assert_eq!(config.http.display_name.as_deref(), Some("Runtime Alpha")); - assert_eq!(config.profile.as_deref(), Some("builtin:coder")); assert_eq!(config.http.local_token.as_deref(), Some("secret-token")); } diff --git a/crates/worker-runtime/src/worker_backend.rs b/crates/worker-runtime/src/worker_backend.rs index 89ee8a48..113e40b8 100644 --- a/crates/worker-runtime/src/worker_backend.rs +++ b/crates/worker-runtime/src/worker_backend.rs @@ -105,7 +105,6 @@ pub struct ProfileRuntimeWorkerFactory { profile_base_dir: PathBuf, store_dir: Option, worker_metadata_dir: Option, - profile: Option, runtime_base_dir: RuntimeArtifactRoot, resource_client: Option>, profile_archive_cache: Arc, @@ -118,7 +117,6 @@ impl ProfileRuntimeWorkerFactory { profile_base_dir, store_dir: None, worker_metadata_dir: None, - profile: None, runtime_base_dir: RuntimeArtifactRoot::owned(), resource_client: None, profile_archive_cache: Arc::new(ProfileSourceArchiveCache::default()), @@ -135,13 +133,6 @@ impl ProfileRuntimeWorkerFactory { self } - /// Set the profile selector used for Runtime-created Workers. When unset, - /// normal default profile discovery is used. - pub fn with_profile(mut self, profile: impl Into) -> Self { - self.profile = Some(profile.into()); - self - } - pub fn with_runtime_base_dir(mut self, runtime_base_dir: impl Into) -> Self { self.runtime_base_dir = RuntimeArtifactRoot::External(runtime_base_dir.into()); self @@ -184,37 +175,27 @@ impl ProfileRuntimeWorkerFactory { fn runtime_profile_value( profile: &crate::catalog::ProfileSelector, - ) -> Option> { + ) -> std::borrow::Cow<'_, str> { match profile { - crate::catalog::ProfileSelector::RuntimeDefault => None, crate::catalog::ProfileSelector::Named(name) => { - Some(std::borrow::Cow::Borrowed(name.as_str())) + std::borrow::Cow::Borrowed(name.as_str()) } crate::catalog::ProfileSelector::Builtin(name) => { if name.starts_with("builtin:") { - Some(std::borrow::Cow::Borrowed(name.as_str())) + std::borrow::Cow::Borrowed(name.as_str()) } else { - Some(std::borrow::Cow::Owned(format!("builtin:{name}"))) + std::borrow::Cow::Owned(format!("builtin:{name}")) } } } } - fn runtime_profile_for_request<'a>( - &'a self, - request: &'a CreateWorkerRequest, - ) -> Option> { - if let Some(profile) = self.profile.as_deref() { - return Some(std::borrow::Cow::Borrowed(profile)); - } + fn runtime_profile_for_request(request: &CreateWorkerRequest) -> std::borrow::Cow<'_, str> { Self::runtime_profile_value(&request.profile) } - fn runtime_profile<'a>( - &'a self, - request: &'a WorkerExecutionSpawnRequest, - ) -> Option> { - self.runtime_profile_for_request(&request.request) + fn runtime_profile(request: &WorkerExecutionSpawnRequest) -> std::borrow::Cow<'_, str> { + Self::runtime_profile_for_request(&request.request) } fn restore_fallback_manifest( @@ -368,7 +349,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory { request: WorkerExecutionSpawnRequest, ) -> Result { let worker_name = Self::runtime_worker_name(&request); - let profile = self.runtime_profile(&request); + let profile = Self::runtime_profile(&request); let has_local_filesystem = request.working_directory.is_some(); let worker_root = request .working_directory @@ -388,7 +369,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory { let workspace_backend_ref = RuntimeWorkspaceBackendRef::from_worker_request(&request.request); let workspace_context = workspace_backend_ref.worker_context(); - let selector = profile.as_deref().unwrap_or("builtin:default"); + let selector = profile.as_ref(); let archive = self .resolve_profile_source_archive(&request.request.profile_source) .await?; @@ -1421,7 +1402,7 @@ mod tests { }, }, profiles: vec![crate::config_bundle::ConfigProfileDescriptor { - selector: ProfileSelector::RuntimeDefault, + selector: ProfileSelector::Builtin("builtin:companion".to_string()), label: Some("adapter-test".to_string()), }], declarations: Vec::new(), @@ -1457,7 +1438,7 @@ mod tests { fn create_request(_name: &str) -> CreateWorkerRequest { let bundle = test_bundle(); CreateWorkerRequest { - profile: ProfileSelector::RuntimeDefault, + profile: ProfileSelector::Builtin("builtin:companion".to_string()), display_name: None, profile_source: crate::catalog::ProfileSourceArchiveSource::Embedded { archive: bundle.profile_source_archive.clone().unwrap(), @@ -1654,15 +1635,15 @@ mod tests { ProfileRuntimeWorkerFactory::runtime_profile_value( &crate::catalog::ProfileSelector::Builtin("coder".to_string()) ) - .as_deref(), - Some("builtin:coder") + .as_ref(), + "builtin:coder" ); assert_eq!( ProfileRuntimeWorkerFactory::runtime_profile_value( &crate::catalog::ProfileSelector::Builtin("builtin:coder".to_string()) ) - .as_deref(), - Some("builtin:coder") + .as_ref(), + "builtin:coder" ); } diff --git a/crates/worker/src/spawn/tool.rs b/crates/worker/src/spawn/tool.rs index 8f2c0ccb..6bd00454 100644 --- a/crates/worker/src/spawn/tool.rs +++ b/crates/worker/src/spawn/tool.rs @@ -45,7 +45,7 @@ struct SpawnWorkerInput { /// Profile selector for child role configuration. Omit or use `default` /// for the effective child default profile, use `inherit` to derive /// reusable config from the spawner, or use a registry selector such as - /// `project:coder`, `project:reviewer`, `builtin:default`, or an + /// `project:coder`, `project:reviewer`, `builtin:companion`, or an /// unambiguous profile slug. Raw/path selectors are rejected. #[serde(default)] profile: Option, @@ -1516,7 +1516,7 @@ max_tokens = 3333 assert!(invalid.contains("Use `default`, `inherit`")); assert!(invalid.contains("`project:coder`")); - let default_config = build_spawn_config_json_for_profile( + let default_error = build_spawn_config_json_for_profile( &parent, &available, &project, @@ -1525,8 +1525,8 @@ max_tokens = 3333 &scope, SpawnProfileSelector::Default, ) - .unwrap(); - assert!(default_config.contains("\"name\":\"child\"")); + .unwrap_err(); + assert!(default_error.contains("no default profile is configured")); let user_config = tmp.path().join("user-profiles.toml"); std::fs::write(&user_config, "[profile]\ncoder = \"user-coder.toml\"\n").unwrap(); diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index aec8cdc5..3e891174 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -234,7 +234,6 @@ pub struct WorkerSummary { pub display_name: String, /// Backward-compatible display label. New UI should prefer `display_name`. pub label: String, - pub role: Option, pub profile: Option, pub singleton_key: Option, #[serde(default)] @@ -274,8 +273,7 @@ impl RuntimeList { } fn is_retired_companion_worker(worker: &WorkerSummary) -> bool { - worker.role.as_deref() == Some("builtin:companion") - || worker.profile.as_deref() == Some("builtin:companion") + worker.profile.as_deref() == Some("builtin:companion") } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -321,8 +319,7 @@ pub struct WorkerSpawnRequest { #[serde(skip_serializing_if = "Option::is_none")] pub requested_worker_name: Option, pub acceptance: WorkerSpawnAcceptanceRequirement, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub profile: Option, + pub profile: ProfileSelector, #[serde(default, skip_serializing_if = "Option::is_none")] pub initial_input: Option, /// Optional safe working-directory creation request. The Workspace server resolves @@ -1405,7 +1402,6 @@ impl EmbeddedWorkerRuntime { host_id: self.host_id.clone(), display_name: display.display_name.clone(), label: display.display_name, - role: profile.clone(), profile, singleton_key: display.singleton_key, tags: display.tags, @@ -1445,7 +1441,6 @@ impl EmbeddedWorkerRuntime { host_id: self.host_id.clone(), display_name: display.display_name.clone(), label: display.display_name, - role: profile.clone(), profile, singleton_key: display.singleton_key, tags: display.tags, @@ -1705,11 +1700,8 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime { )); } - let profile = request - .profile - .clone() - .unwrap_or_else(|| embedded_profile_selector(&request.intent)); - let profile_source = match default_profile_source_archive_source(&profile) { + let profile = request.profile.clone(); + let profile_source = match profile_source_archive_source(&request, &profile) { Ok(source) => source, Err(error) => { diagnostics.push(diagnostic( @@ -2364,7 +2356,6 @@ impl RemoteWorkerRuntime { host_id: self.host_id.clone(), display_name: display.display_name.clone(), label: display.display_name, - role: profile.clone(), profile, singleton_key: display.singleton_key, tags: display.tags, @@ -2408,7 +2399,6 @@ impl RemoteWorkerRuntime { host_id: self.host_id.clone(), display_name: display.display_name.clone(), label: display.display_name, - role: profile.clone(), profile, singleton_key: display.singleton_key, tags: display.tags, @@ -2678,11 +2668,9 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime { )], }; } - let profile = request - .profile - .clone() - .unwrap_or_else(|| embedded_profile_selector(&request.intent)); - let profile_source = match default_profile_source_archive_http_source( + let profile = request.profile.clone(); + let profile_source = match profile_source_archive_http_source( + &request, &profile, &self.workspace_id, Some(self.runtime_id.as_str()), @@ -2953,22 +2941,38 @@ fn embedded_worker_projection_diagnostics() -> Vec { )] } -fn default_profile_source_archive_source( +fn profile_source_archive_for_request( + request: &WorkerSpawnRequest, + profile: &ProfileSelector, +) -> Result { + if let Some(archive) = request + .resolved_config_bundle + .as_ref() + .and_then(|bundle| bundle.profile_source_archive.clone()) + { + return Ok(archive); + } + builtin_profile_source_archive(profile) +} + +fn profile_source_archive_source( + request: &WorkerSpawnRequest, profile: &ProfileSelector, ) -> Result { Ok(ProfileSourceArchiveSource::Embedded { - archive: default_profile_source_archive(profile)?, + archive: profile_source_archive_for_request(request, profile)?, }) } -fn default_profile_source_archive_http_source( +fn profile_source_archive_http_source( + request: &WorkerSpawnRequest, profile: &ProfileSelector, workspace_id: &str, runtime_id: Option<&str>, resource_broker: &BackendResourceBroker, backend_base_url: &str, ) -> Result { - let archive = default_profile_source_archive(profile)?; + let archive = profile_source_archive_for_request(request, profile)?; let _handle = resource_broker.issue_profile_source_archive_handle( workspace_id.to_string(), runtime_id, @@ -2999,7 +3003,7 @@ enum ProfileSourceArchiveTransport { } #[cfg(test)] -fn default_embedded_config_bundle( +fn builtin_profile_config_bundle( profile: &ProfileSelector, workspace_id: &str, runtime_id: Option<&str>, @@ -3012,7 +3016,7 @@ fn default_embedded_config_bundle( .unwrap_or_else(|| "default".to_string()) .replace([':', '/', ' '], "-") ); - let archive = default_profile_source_archive(profile)?; + let archive = builtin_profile_source_archive(profile)?; let (profile_source_archive, profile_source_archive_handle) = match archive_transport { ProfileSourceArchiveTransport::Inline => (Some(archive), None), ProfileSourceArchiveTransport::BackendResourceHandle => { @@ -3048,17 +3052,13 @@ fn default_embedded_config_bundle( .with_computed_digest()) } -fn default_profile_source_archive( +fn builtin_profile_source_archive( profile: &ProfileSelector, ) -> Result { - let selected = embedded_profile_label(profile).unwrap_or_else(|| "default".to_string()); + let selected = embedded_profile_label(profile) + .ok_or_else(|| "profile selector must identify a concrete profile".to_string())?; let selected_path = embedded_profile_path(profile)?; let mut entrypoints = BTreeMap::new(); - entrypoints.insert("default".to_string(), "profiles/default.dcdl".to_string()); - entrypoints.insert( - "builtin:default".to_string(), - "profiles/default.dcdl".to_string(), - ); for slug in [ "companion", "intake", @@ -3073,8 +3073,8 @@ fn default_profile_source_archive( let mut sources = BTreeMap::new(); sources.insert( - "profiles/default.dcdl".to_string(), - include_str!("../../../resources/profiles/default.dcdl").to_string(), + "profiles/base.dcdl".to_string(), + include_str!("../../../resources/profiles/base.dcdl").to_string(), ); sources.insert( "profiles/companion.dcdl".to_string(), @@ -3111,8 +3111,8 @@ fn default_profile_source_archive( "memory-consolidation", ] { imports.insert( - format!("profiles/{slug}.dcdl\0./default.dcdl"), - "profiles/default.dcdl".to_string(), + format!("profiles/{slug}.dcdl\0./base.dcdl"), + "profiles/base.dcdl".to_string(), ); } @@ -3127,9 +3127,7 @@ fn default_profile_source_archive( fn embedded_profile_path(profile: &ProfileSelector) -> Result { match profile { - ProfileSelector::RuntimeDefault => Ok("profiles/default.dcdl".to_string()), ProfileSelector::Builtin(name) => match name.strip_prefix("builtin:").unwrap_or(name) { - "default" => Ok("profiles/default.dcdl".to_string()), "companion" => Ok("profiles/companion.dcdl".to_string()), "intake" => Ok("profiles/intake.dcdl".to_string()), "orchestrator" => Ok("profiles/orchestrator.dcdl".to_string()), @@ -3142,31 +3140,8 @@ fn embedded_profile_path(profile: &ProfileSelector) -> Result { } } -fn embedded_profile_selector(intent: &WorkerSpawnIntent) -> ProfileSelector { - match intent { - WorkerSpawnIntent::TicketRole { role, .. } => { - ProfileSelector::Builtin(format!("builtin:{}", ticket_role_profile_slug(role))) - } - WorkerSpawnIntent::WorkspaceCompanion => { - ProfileSelector::Builtin("builtin:companion".to_string()) - } - WorkerSpawnIntent::WorkspaceOrchestrator => ProfileSelector::RuntimeDefault, - WorkerSpawnIntent::WorkspaceCoding => ProfileSelector::Builtin("builtin:coder".to_string()), - } -} - -fn ticket_role_profile_slug(role: &TicketWorkerRole) -> &'static str { - match role { - TicketWorkerRole::Intake => "intake", - TicketWorkerRole::Orchestrator => "orchestrator", - TicketWorkerRole::Coder => "coder", - TicketWorkerRole::Reviewer => "reviewer", - } -} - fn embedded_profile_label(profile: &ProfileSelector) -> Option { Some(match profile { - ProfileSelector::RuntimeDefault => "runtime_default".to_string(), ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => { if name.strip_prefix("builtin:").unwrap_or(name) == MEMORY_CONSOLIDATION_PROFILE { MEMORY_CONSOLIDATION_PROFILE.to_string() @@ -3227,21 +3202,18 @@ fn worker_display_metadata( } fn profile_display_name(profile_label: &str) -> String { - match profile_label { - "runtime_default" => "Default Worker".to_string(), - value => value - .split(['-', '_']) - .filter(|part| !part.is_empty()) - .map(|part| { - let mut chars = part.chars(); - match chars.next() { - Some(first) => first.to_uppercase().chain(chars).collect::(), - None => String::new(), - } - }) - .collect::>() - .join(" "), - } + profile_label + .split(['-', '_']) + .filter(|part| !part.is_empty()) + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + Some(first) => first.to_uppercase().chain(chars).collect::(), + None => String::new(), + } + }) + .collect::>() + .join(" ") } fn embedded_input_rejected( @@ -3632,7 +3604,6 @@ pub fn placeholder_worker(host_id: impl Into) -> WorkerSummary { host_id, display_name: "Worker runtime actions are not implemented".to_string(), label: "Worker runtime actions are not implemented".to_string(), - role: None, profile: None, singleton_key: None, tags: Vec::new(), @@ -3690,7 +3661,6 @@ mod tests { let broker = BackendResourceBroker::default(); let runtime_id = "runtime-test"; for selector in [ - ProfileSelector::RuntimeDefault, ProfileSelector::Builtin("builtin:companion".to_string()), ProfileSelector::Builtin("builtin:intake".to_string()), ProfileSelector::Builtin("builtin:orchestrator".to_string()), @@ -3698,7 +3668,7 @@ mod tests { ProfileSelector::Builtin("builtin:reviewer".to_string()), ProfileSelector::Builtin("builtin:memory-consolidation".to_string()), ] { - let bundle = default_embedded_config_bundle( + let bundle = builtin_profile_config_bundle( &selector, "workspace-test", Some(runtime_id), @@ -3724,9 +3694,7 @@ mod tests { .verify() .unwrap(); let selector_key = match &selector { - ProfileSelector::RuntimeDefault => "default".to_string(), - ProfileSelector::Builtin(name) => name.clone(), - ProfileSelector::Named(name) => name.clone(), + ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => name.clone(), }; let manifest = archive .resolve_profile(&selector_key, root.path(), "embedded-test-worker") @@ -3745,7 +3713,7 @@ mod tests { let root = tempfile::tempdir().unwrap(); let broker = BackendResourceBroker::default(); let runtime_id = "remote:test"; - let bundle = default_embedded_config_bundle( + let bundle = builtin_profile_config_bundle( &ProfileSelector::Builtin("builtin:coder".to_string()), "workspace-test", Some(runtime_id), @@ -3757,7 +3725,7 @@ mod tests { let archive = bundle .profile_source_archive .as_ref() - .expect("remote default bundle carries inline profile archive") + .expect("remote built-in bundle carries inline profile archive") .verify() .unwrap(); let manifest = archive @@ -3766,11 +3734,38 @@ mod tests { assert_eq!(manifest.worker.name, "remote-test-worker"); } + #[test] + fn resolved_project_profile_archive_is_used_for_runtime_delivery() { + let broker = BackendResourceBroker::default(); + let builtin_selector = ProfileSelector::Builtin("builtin:coder".to_string()); + let archive = builtin_profile_source_archive(&builtin_selector) + .expect("build stand-in project profile archive"); + let mut bundle = builtin_profile_config_bundle( + &builtin_selector, + "workspace-test", + Some("runtime-test"), + &broker, + ProfileSourceArchiveTransport::Inline, + ) + .expect("build project profile bundle"); + bundle.profile_source_archive = Some(archive.clone()); + let mut request = embedded_spawn_request(); + request.profile = ProfileSelector::Named("project:custom".to_string()); + request.resolved_config_bundle = Some(bundle); + + let delivered = profile_source_archive_for_request(&request, &request.profile) + .expect("resolve project profile archive"); + assert_eq!(delivered.reference, archive.reference); + assert_eq!(delivered.content, archive.content); + } + #[test] fn remote_profile_source_archive_url_uses_workspace_id_not_host_id() { let broker = BackendResourceBroker::default(); let runtime_id = "remote:test"; - let source = default_profile_source_archive_http_source( + let request = embedded_spawn_request(); + let source = profile_source_archive_http_source( + &request, &ProfileSelector::Builtin("builtin:coder".to_string()), "workspace-actual", Some(runtime_id), @@ -3796,7 +3791,7 @@ mod tests { let broker = BackendResourceBroker::default(); let runtime_id = "runtime-test"; assert!( - default_embedded_config_bundle( + builtin_profile_config_bundle( &ProfileSelector::Builtin("builtin:missing".to_string()), "workspace-test", Some(runtime_id), @@ -3806,7 +3801,7 @@ mod tests { .is_err() ); assert!( - default_embedded_config_bundle( + builtin_profile_config_bundle( &ProfileSelector::Named("custom".to_string()), "workspace-test", Some(runtime_id), @@ -3966,7 +3961,6 @@ mod tests { host_id: host_id.to_string(), display_name: label.to_string(), label: label.to_string(), - role: None, profile: None, singleton_key: None, tags: Vec::new(), @@ -4158,7 +4152,7 @@ mod tests { acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments: 0, }, - profile: None, + profile: ProfileSelector::Builtin("builtin:coder".to_string()), initial_input: None, working_directory_request: None, resolved_working_directory_request: None, @@ -4284,7 +4278,7 @@ mod tests { acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments: 0, }, - profile: None, + profile: ProfileSelector::Builtin("builtin:coder".to_string()), initial_input: None, working_directory_request: None, resolved_working_directory_request: None, @@ -4380,7 +4374,7 @@ mod tests { acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments: 0, }, - profile: Some(ProfileSelector::Builtin("builtin:coder".to_string())), + profile: ProfileSelector::Builtin("builtin:coder".to_string()), initial_input: None, working_directory_request: None, resolved_working_directory_request: None, @@ -4412,7 +4406,7 @@ mod tests { intent: WorkerSpawnIntent::WorkspaceCompanion, requested_worker_name: None, acceptance: WorkerSpawnAcceptanceRequirement::SocketReady, - profile: None, + profile: ProfileSelector::Builtin("builtin:companion".to_string()), initial_input: None, working_directory_request: None, resolved_working_directory_request: None, diff --git a/crates/workspace-server/src/profile_settings.rs b/crates/workspace-server/src/profile_settings.rs index 4f105c68..be21be8d 100644 --- a/crates/workspace-server/src/profile_settings.rs +++ b/crates/workspace-server/src/profile_settings.rs @@ -19,21 +19,14 @@ const PROFILE_SOURCE_TREE_ID: &str = "project"; const PROFILE_SOURCE_TREE_DISPLAY_ROOT: &str = "profiles"; const MAX_PROFILE_SOURCE_BYTES: u64 = 256 * 1024; const BUILTIN_PROFILE_IDS: &[&str] = &[ - "builtin:default", "builtin:companion", "builtin:intake", "builtin:orchestrator", "builtin:coder", "builtin:reviewer", ]; -const BUILTIN_PROFILE_SLUGS: &[&str] = &[ - "default", - "companion", - "intake", - "orchestrator", - "coder", - "reviewer", -]; +const BUILTIN_PROFILE_SLUGS: &[&str] = + &["companion", "intake", "orchestrator", "coder", "reviewer"]; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkspaceMetadataSettingsResponse { @@ -832,7 +825,6 @@ pub fn project_profile_candidates(workspace_root: &Path) -> Vec bool { BUILTIN_PROFILE_IDS.contains(&profile_id) - || profile_id == "runtime_default" || project_profile_candidates(workspace_root) .into_iter() .any(|profile| profile.profile_id == profile_id) @@ -840,7 +832,6 @@ pub fn is_profile_candidate(workspace_root: &Path, profile_id: &str) -> bool { fn builtin_profile_summaries(default_profile: Option<&str>) -> Vec { let labels = [ - ("builtin:default", "Default", "Bundled default Yoi profile"), ( "builtin:companion", "Companion", @@ -1191,15 +1182,6 @@ fn build_profile_archive_from_tree_sources( entrypoints.insert(project_selector(&entry.name), path); } } - if let Some(default) = registry - .default - .as_deref() - .filter(|value| value.starts_with("project:")) - { - if let Some(path) = entrypoints.get(default).cloned() { - entrypoints.insert("default".to_string(), path); - } - } build_profile_archive_from_source_set(entrypoints, sources) } @@ -1920,9 +1902,7 @@ pub fn selector_for_builtin_candidate( id: &str, ) -> Option { match id { - "runtime_default" => Some(worker_runtime::catalog::ProfileSelector::RuntimeDefault), - "builtin:default" - | "builtin:companion" + "builtin:companion" | "builtin:intake" | "builtin:orchestrator" | "builtin:coder" diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 3ee955db..f98bdd50 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -1059,6 +1059,7 @@ pub struct RemoteRuntimeTestResponse { pub struct WorkerLaunchOptionsResponse { pub workspace_id: String, pub runtimes: Vec, + pub default_profile: Option, pub profiles: Vec, pub repositories: Vec, pub working_directories: Vec, @@ -1128,7 +1129,8 @@ pub struct BrowserWorkerWorkingDirectorySelection { pub struct BrowserCreateWorkerRequest { pub runtime_id: String, pub display_name: String, - pub profile: String, + #[serde(default)] + pub profile: Option, pub initial_text: String, #[serde(default)] pub working_directory: Option, @@ -1679,7 +1681,7 @@ fn start_memory_staging_consolidation( acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments: 1, }, - profile: Some(profile_selector), + profile: profile_selector, initial_input: Some(input), working_directory_request: None, resolved_working_directory_request: None, @@ -1788,10 +1790,7 @@ fn try_reuse_memory_consolidation_worker( fn is_memory_consolidation_worker(worker: &WorkerSummary) -> bool { worker.singleton_key.as_deref() == Some(MEMORY_CONSOLIDATION_SINGLETON_KEY) - || [worker.profile.as_deref(), worker.role.as_deref()] - .into_iter() - .flatten() - .any(|value| value == MEMORY_CONSOLIDATION_PROFILE) + || worker.profile.as_deref() == Some(MEMORY_CONSOLIDATION_PROFILE) } fn memory_consolidation_input_content(candidate_count: usize, total_bytes: u64) -> String { @@ -4261,20 +4260,40 @@ async fn create_workspace_worker( State(api): State, Json(request): Json, ) -> ApiResult> { + let profile = request + .profile + .as_deref() + .map(str::trim) + .filter(|profile| !profile.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| { + crate::profile_settings::load_profile_settings( + &api.config.workspace_id, + &api.config.workspace_root, + ) + .default_profile + }) + .ok_or_else(|| { + settings_bad_request( + "workspace_default_profile_missing", + "profile is required because this Workspace has no default profile configured", + ) + })?; let profile_selector = - profile_selector_for_candidate_with_root(&api.config.workspace_root, &request.profile) - .ok_or_else(|| { + profile_selector_for_candidate_with_root(&api.config.workspace_root, &profile).ok_or_else( + || { settings_bad_request( "unsupported_worker_profile", "profile must be selected from Backend-published worker profile candidates", ) - })?; - let resolved_config_bundle = if request.profile.starts_with("project:") { + }, + )?; + let resolved_config_bundle = if profile.starts_with("project:") { crate::profile_settings::build_workspace_profile_config_bundle( &api.config.workspace_root, &api.config.workspace_id, &api.config.workspace_created_at, - &request.profile, + &profile, )? } else { None @@ -4320,7 +4339,7 @@ async fn create_workspace_worker( acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments: if initial_input.is_some() { 1 } else { 0 }, }, - profile: Some(profile_selector), + profile: profile_selector, initial_input, working_directory_request: None, resolved_working_directory_request: None, @@ -5773,10 +5792,32 @@ fn worker_launch_options_response(api: &WorkspaceApi) -> WorkerLaunchOptionsResp } }) .collect(); + let profile_settings = crate::profile_settings::load_profile_settings( + &api.config.workspace_id, + &api.config.workspace_root, + ); + let profiles = profile_settings + .profiles + .into_iter() + .filter(|profile| { + !profile + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error) + }) + .map(|profile| WorkerLaunchProfileCandidate { + id: profile.profile_id, + label: profile.label, + description: profile + .description + .unwrap_or_else(|| "Workspace profile.".to_string()), + }) + .collect(); WorkerLaunchOptionsResponse { workspace_id: api.config.workspace_id.clone(), runtimes, - profiles: worker_profile_candidates_for_root(&api.config.workspace_root), + default_profile: profile_settings.default_profile, + profiles, repositories: working_directory_repository_options(api), working_directories: available_working_directory_summaries(api).unwrap_or_default(), diagnostics: Vec::new(), @@ -5932,7 +5973,6 @@ fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary worker_id: record.runtime_worker_id.to_string(), runtime_id: record.runtime_id.clone(), host_id: "backend-registry".to_string(), - role: None, display_name: record.display_name.clone(), label: record.display_name.clone(), singleton_key: None, @@ -6373,58 +6413,25 @@ fn working_directory_request_for_browser( }) } -#[cfg(test)] -fn worker_profile_candidates() -> Vec { - worker_profile_candidates_for_root(Path::new(".")) - .into_iter() - .filter(|candidate| candidate.id == "builtin:coder" || candidate.id == "runtime_default") - .collect() -} - -fn worker_profile_candidates_for_root(workspace_root: &Path) -> Vec { - let mut candidates = vec![ - WorkerLaunchProfileCandidate { - id: "builtin:coder".to_string(), - label: "Coding Worker".to_string(), - description: "Built-in coding role profile for implementation work.".to_string(), - }, - WorkerLaunchProfileCandidate { - id: "runtime_default".to_string(), - label: "Runtime default".to_string(), - description: "Use the selected Runtime's default profile.".to_string(), - }, - ]; - candidates.extend( - crate::profile_settings::project_profile_candidates(workspace_root) - .into_iter() - .filter(|profile| profile.diagnostics.is_empty()) - .map(|profile| WorkerLaunchProfileCandidate { - id: profile.profile_id, - label: profile.label, - description: profile - .description - .unwrap_or_else(|| "Workspace Decodal profile source.".to_string()), - }), - ); - candidates -} - -fn profile_selector_for_candidate(profile: &str) -> Option { - crate::profile_settings::selector_for_builtin_candidate(profile) - .filter(|_| matches!(profile, "builtin:coder" | "runtime_default")) -} - fn profile_selector_for_candidate_with_root( workspace_root: &Path, profile: &str, ) -> Option { - if profile_selector_for_candidate(profile).is_some() { - profile_selector_for_candidate(profile) - } else if crate::profile_settings::is_profile_candidate(workspace_root, profile) { + if let Some(selector @ ProfileSelector::Builtin(_)) = crate::profile_settings::selector_for_builtin_candidate(profile) - } else { - None + { + return Some(selector); } + crate::profile_settings::project_profile_candidates(workspace_root) + .into_iter() + .find(|candidate| { + candidate.profile_id == profile + && !candidate + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error) + }) + .and_then(|_| crate::profile_settings::selector_for_builtin_candidate(profile)) } fn parse_runtime_worker_id_for_registry(worker_id: &str) -> ApiResult { @@ -6441,7 +6448,7 @@ fn sanitize_worker_display_name(value: &str) -> Option { if display_name.chars().any(char::is_control) { None } else if display_name.is_empty() { - Some("Coding Worker".to_string()) + Some("Worker".to_string()) } else { Some(display_name.chars().take(80).collect()) } @@ -7065,17 +7072,89 @@ mod tests { #[test] fn worker_profile_candidates_are_backend_published_and_mapped() { - let candidates = worker_profile_candidates(); + let dir = tempfile::tempdir().unwrap(); + fs::create_dir_all(dir.path().join(".yoi")).unwrap(); + fs::write( + dir.path().join(".yoi/profiles.toml"), + "default = \"builtin:companion\"\n", + ) + .unwrap(); + let settings = crate::profile_settings::load_profile_settings("workspace-test", dir.path()); + for expected in [ + "builtin:companion", + "builtin:intake", + "builtin:orchestrator", + "builtin:coder", + "builtin:reviewer", + ] { + assert!( + settings + .profiles + .iter() + .any(|profile| profile.profile_id == expected), + "missing {expected}" + ); + } assert!( - candidates + settings + .profiles .iter() - .any(|candidate| candidate.id == "builtin:coder") + .all(|profile| profile.profile_id != "builtin:default") + ); + assert_eq!( + settings.default_profile.as_deref(), + Some("builtin:companion") ); assert!(matches!( - profile_selector_for_candidate("builtin:coder"), + profile_selector_for_candidate_with_root(dir.path(), "builtin:coder"), Some(ProfileSelector::Builtin(value)) if value == "builtin:coder" )); - assert!(profile_selector_for_candidate("free-text-profile").is_none()); + assert!( + profile_selector_for_candidate_with_root(dir.path(), "free-text-profile").is_none() + ); + } + + #[tokio::test] + async fn worker_launch_options_publish_every_valid_workspace_profile_and_default() { + let dir = tempfile::tempdir().unwrap(); + fs::create_dir_all(dir.path().join(".yoi/profiles")).unwrap(); + fs::write( + dir.path().join(".yoi/profiles.toml"), + concat!( + "default = \"builtin:companion\"\n", + "[profile.custom]\n", + "path = \".yoi/profiles/custom.dcdl\"\n", + ), + ) + .unwrap(); + fs::write( + dir.path().join(".yoi/profiles/custom.dcdl"), + "slug = \"custom\"; description = \"Custom profile\";\n", + ) + .unwrap(); + let api = test_app(dir.path()).await; + + let response = get_json(api, "/api/workers/launch-options").await; + assert_eq!(response["default_profile"], "builtin:companion"); + let profiles = response["profiles"].as_array().unwrap(); + for expected in [ + "builtin:companion", + "builtin:intake", + "builtin:orchestrator", + "builtin:coder", + "builtin:reviewer", + "project:custom", + ] { + assert!( + profiles.iter().any(|profile| profile["id"] == expected), + "missing {expected}: {profiles:?}" + ); + } + assert!( + profiles + .iter() + .all(|profile| profile["id"] != "builtin:default") + ); } #[test] @@ -7258,11 +7337,19 @@ mod tests { ) .unwrap(); fs::write(dir.path().join(".yoi/profiles/bad.dcdl"), "not decodal").unwrap(); - assert!( - worker_profile_candidates_for_root(dir.path()) - .iter() - .all(|candidate| candidate.id != "project:bad") - ); + let candidates = + crate::profile_settings::load_profile_settings("workspace-test", dir.path()) + .profiles + .into_iter() + .filter(|profile| { + !profile + .diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error) + }) + .map(|profile| profile.profile_id) + .collect::>(); + assert!(!candidates.iter().any(|profile| profile == "project:bad")); assert!(profile_selector_for_candidate_with_root(dir.path(), "project:bad").is_none()); } @@ -7583,9 +7670,7 @@ mod tests { acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments: 0, }, - profile: Some(ProfileSelector::Builtin( - MEMORY_CONSOLIDATION_PROFILE.to_string(), - )), + profile: ProfileSelector::Builtin(MEMORY_CONSOLIDATION_PROFILE.to_string()), initial_input: None, working_directory_request: None, resolved_working_directory_request: None, @@ -8141,7 +8226,9 @@ mod tests { }, }, profiles: vec![worker_runtime::config_bundle::ConfigProfileDescriptor { - selector: worker_runtime::catalog::ProfileSelector::RuntimeDefault, + selector: worker_runtime::catalog::ProfileSelector::Builtin( + "builtin:companion".to_string(), + ), label: Some("server-test".to_string()), }], declarations: Vec::new(), @@ -8154,7 +8241,9 @@ mod tests { fn runtime_create_request() -> worker_runtime::catalog::CreateWorkerRequest { let bundle = runtime_test_bundle(); worker_runtime::catalog::CreateWorkerRequest { - profile: worker_runtime::catalog::ProfileSelector::RuntimeDefault, + profile: worker_runtime::catalog::ProfileSelector::Builtin( + "builtin:companion".to_string(), + ), display_name: None, profile_source: worker_runtime::catalog::ProfileSourceArchiveSource::Http { location: worker_runtime::catalog::ProfileSourceArchiveHttpRef { @@ -8534,8 +8623,14 @@ mod tests { } #[tokio::test] - async fn browser_worker_create_succeeds_and_preserves_unsupported_diagnostics() { + async fn browser_worker_create_uses_workspace_default_and_preserves_unsupported_diagnostics() { let dir = tempfile::tempdir().unwrap(); + fs::create_dir_all(dir.path().join(".yoi")).unwrap(); + fs::write( + dir.path().join(".yoi/profiles.toml"), + "default = \"builtin:coder\"\n", + ) + .unwrap(); let app = test_app(dir.path()).await; let created = post_json( app.clone(), @@ -8543,7 +8638,6 @@ mod tests { serde_json::json!({ "runtime_id": "embedded-worker-runtime", "display_name": "", - "profile": "runtime_default", "initial_text": "" }), ) @@ -8556,7 +8650,9 @@ mod tests { .iter() .find(|worker| worker["worker_id"] == created["worker_id"]) .expect("created Worker should be listed"); - assert_eq!(worker["label"], "Coding Worker"); + assert_eq!(worker["label"], "Worker"); + assert_eq!(worker["profile"], "builtin:coder"); + assert!(worker.get("role").is_none()); assert_eq!(worker["worker_id"], created["worker_id"]); let detail_path = format!( "/api/runtimes/{}/workers/{}", @@ -8564,7 +8660,7 @@ mod tests { created["worker_id"].as_str().unwrap() ); let detail = get_json(app.clone(), detail_path.as_str()).await; - assert_eq!(detail["label"], "Coding Worker"); + assert_eq!(detail["label"], "Worker"); assert_eq!(detail["worker_id"], created["worker_id"]); assert!( created["console_href"] @@ -8607,7 +8703,7 @@ mod tests { Some(serde_json::json!({ "runtime_id": "remote-runtime", "display_name": "Remote Worker", - "profile": "runtime_default", + "profile": "builtin:companion", "initial_text": "" })), StatusCode::BAD_REQUEST, @@ -8646,6 +8742,10 @@ mod tests { "kind": "run_accepted", "expected_segments": 0 }, + "profile": { + "kind": "builtin", + "value": "builtin:coder" + }, "working_directory_request": { "repository_id": TEST_REPOSITORY_ID, "local_path": dir.path().display().to_string() @@ -8681,6 +8781,10 @@ mod tests { "kind": "run_accepted", "expected_segments": 0 }, + "profile": { + "kind": "builtin", + "value": "builtin:coder" + }, "working_directory_request": { "repository_id": TEST_REPOSITORY_ID, "selector": "HEAD" @@ -9259,7 +9363,7 @@ mod tests { acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments: 0, }, - profile: None, + profile: ProfileSelector::Builtin("builtin:coder".to_string()), initial_input: None, working_directory_request: None, resolved_working_directory_request: None, @@ -9501,6 +9605,10 @@ mod tests { "acceptance": { "kind": "run_accepted", "expected_segments": 0 + }, + "profile": { + "kind": "builtin", + "value": "builtin:coder" } }), ) diff --git a/docs/report/2026-07-30-spawn-worker-runtime-cli-drift.md b/docs/report/2026-07-30-spawn-worker-runtime-cli-drift.md new file mode 100644 index 00000000..11eadc7a --- /dev/null +++ b/docs/report/2026-07-30-spawn-worker-runtime-cli-drift.md @@ -0,0 +1,29 @@ +# SpawnWorker failed because the active Runtime CLI rejected its launch protocol + +Date: 2026-07-30 + +## Context + +While implementing Ticket `00001KYS7YMN5`, the parent Worker delegated two read-only code-mapping tasks through `SpawnWorker`. + +Both launches failed before a child socket appeared. The spawned process reported: + +```text +yoi-runtime: unexpected positional argument `worker` + +Usage: yoi-runtime [OPTIONS] +``` + +The active `yoi-runtime` binary accepts only its HTTP Runtime server options and auth subcommands, while the Worker-management layer attempted to start a child through a legacy `worker` positional command. + +## Impact + +- Delegated read-only investigation was unavailable. +- The parent Worker had to perform the code mapping directly. +- This is protocol/version drift between the Worker-management launch path and the active Runtime binary, not a failure in the delegated task or scope. + +## Suggested improvement + +Make the child-launch protocol explicit and versioned rather than invoking a user-facing Runtime CLI subcommand implicitly. At minimum, Runtime registration/health should advertise the supported spawn protocol, and `SpawnWorker` should reject an incompatible Runtime with a bounded compatibility diagnostic before attempting to create a child socket. + +The active backend/runtime should also be rebuilt and restarted together after CLI/launch-protocol changes so long-running Worker-management processes do not retain an obsolete invocation contract. diff --git a/resources/models/builtin.toml b/resources/models/builtin.toml index 010a425f..02435c4f 100644 --- a/resources/models/builtin.toml +++ b/resources/models/builtin.toml @@ -32,6 +32,17 @@ context_window = 256000 # Codex CLI's model catalog exposes these coding models with a 272k effective # context window on this route, even when public API docs advertise larger # model-family windows. +# Codex currently caps the ChatGPT-backed route at 272k even though the public +# GPT-5.6 Sol API model advertises a 1.05M context window. Keep both values so +# resolution and compaction use the effective backend limit without losing the +# underlying model capability. +[[model]] +id = "gpt-5.6-sol" +provider = "codex-oauth" +context_window = 1050000 +max_context_window = 272000 +capability = { tool_calling = "parallel", structured_output = "json_schema", reasoning = "effort", vision = true, prompt_caching = { kind = "auto" } } + [[model]] id = "gpt-5.5" provider = "codex-oauth" diff --git a/resources/profiles/default.dcdl b/resources/profiles/base.dcdl similarity index 86% rename from resources/profiles/default.dcdl rename to resources/profiles/base.dcdl index a8bb72ec..d04d30dd 100644 --- a/resources/profiles/default.dcdl +++ b/resources/profiles/base.dcdl @@ -1,9 +1,9 @@ -slug = "default"; -description = "Default Yoi coding profile."; +slug = "base"; +description = "Shared base configuration for Yoi Worker profiles."; scope = "workspace_write"; model = { - ref = "codex-oauth/gpt-5.5"; + ref = "codex-oauth/gpt-5.6-sol"; }; session = { diff --git a/resources/profiles/coder.dcdl b/resources/profiles/coder.dcdl index e04783c5..4fdcad5e 100644 --- a/resources/profiles/coder.dcdl +++ b/resources/profiles/coder.dcdl @@ -1,4 +1,4 @@ -import "./default.dcdl" // { +import "./base.dcdl" // { slug = "coder"; description = "Ticket implementation coder profile."; scope = "workspace_write"; diff --git a/resources/profiles/companion.dcdl b/resources/profiles/companion.dcdl index 0e84e56e..50e70e1f 100644 --- a/resources/profiles/companion.dcdl +++ b/resources/profiles/companion.dcdl @@ -1,4 +1,4 @@ -import "./default.dcdl" // { +import "./base.dcdl" // { slug = "companion"; description = "Workspace companion profile."; scope = "workspace_write"; diff --git a/resources/profiles/intake.dcdl b/resources/profiles/intake.dcdl index 28bea099..5e88e598 100644 --- a/resources/profiles/intake.dcdl +++ b/resources/profiles/intake.dcdl @@ -1,4 +1,4 @@ -import "./default.dcdl" // { +import "./base.dcdl" // { slug = "intake"; description = "Ticket intake profile."; scope = "workspace_write"; diff --git a/resources/profiles/memory-consolidation.dcdl b/resources/profiles/memory-consolidation.dcdl index 349893ac..34a030eb 100644 --- a/resources/profiles/memory-consolidation.dcdl +++ b/resources/profiles/memory-consolidation.dcdl @@ -1,4 +1,4 @@ -import "./default.dcdl" // { +import "./base.dcdl" // { slug = "memory-consolidation"; description = "Memory staging consolidation profile."; scope = "workspace_read"; diff --git a/resources/profiles/orchestrator.dcdl b/resources/profiles/orchestrator.dcdl index a96df119..29ddd573 100644 --- a/resources/profiles/orchestrator.dcdl +++ b/resources/profiles/orchestrator.dcdl @@ -1,4 +1,4 @@ -import "./default.dcdl" // { +import "./base.dcdl" // { slug = "orchestrator"; description = "Ticket orchestrator profile."; scope = "workspace_write"; diff --git a/resources/profiles/reviewer.dcdl b/resources/profiles/reviewer.dcdl index 1e01c3dd..012a729f 100644 --- a/resources/profiles/reviewer.dcdl +++ b/resources/profiles/reviewer.dcdl @@ -1,4 +1,4 @@ -import "./default.dcdl" // { +import "./base.dcdl" // { slug = "reviewer"; description = "Ticket review profile."; scope = "workspace_read"; diff --git a/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte b/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte index c9b4bd8a..0d4f0fd2 100644 --- a/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte +++ b/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte @@ -109,7 +109,7 @@ - - worker {worker.worker_id} · {worker.role ? `${worker.role} · ` : ''}{worker.state} · 🖥 {worker.host_id} + worker {worker.worker_id} · {worker.profile ? `${worker.profile} · ` : ''}{worker.state} · 🖥 {worker.host_id} {worker.working_directory ? ` · wd:${worker.working_directory.repository_id}@${worker.working_directory.resolved_commit.slice(0, 8)}` : ''} diff --git a/web/workspace/src/lib/workspace/sidebar/types.ts b/web/workspace/src/lib/workspace/sidebar/types.ts index f25c2c1b..022bf95e 100644 --- a/web/workspace/src/lib/workspace/sidebar/types.ts +++ b/web/workspace/src/lib/workspace/sidebar/types.ts @@ -79,7 +79,6 @@ export type Worker = { host_id: string; display_name: string; label: string; - role?: string | null; profile?: string | null; singleton_key?: string | null; tags: string[]; @@ -233,6 +232,7 @@ export type BrowserWorkingDirectoryCreateRequest = { export type WorkerLaunchOptionsResponse = { workspace_id: string; runtimes: WorkerLaunchRuntimeOption[]; + default_profile?: string | null; profiles: WorkerLaunchProfileCandidate[]; repositories: WorkingDirectoryRepositoryOption[]; working_directories: WorkingDirectorySummary[]; diff --git a/web/workspace/src/lib/workspace/sidebar/worker-launch.test.ts b/web/workspace/src/lib/workspace/sidebar/worker-launch.test.ts index 93750e96..ceba9bbf 100644 --- a/web/workspace/src/lib/workspace/sidebar/worker-launch.test.ts +++ b/web/workspace/src/lib/workspace/sidebar/worker-launch.test.ts @@ -38,6 +38,7 @@ const options: WorkerLaunchOptionsResponse = { diagnostics: [], }, ], + default_profile: "builtin:coder", profiles: [ { id: "builtin:companion", label: "Companion", description: "chat" }, { id: "builtin:coder", label: "Coder", description: "code" }, @@ -65,7 +66,7 @@ const options: WorkerLaunchOptionsResponse = { diagnostics: [], }; -Deno.test("defaultWorkerLaunchForm chooses active runtime, coder profile, repository, and working directory", () => { +Deno.test("defaultWorkerLaunchForm uses the Backend-published Workspace default profile", () => { const form = defaultWorkerLaunchForm(options, { runtime_id: "", display_name: "", @@ -78,7 +79,7 @@ Deno.test("defaultWorkerLaunchForm chooses active runtime, coder profile, reposi }); assertEquals(form.runtime_id, "remote"); - assertEquals(form.display_name, "Coding Worker"); + assertEquals(form.display_name, "Worker"); assertEquals(form.profile, "builtin:coder"); assertEquals(form.initial_text, "hello"); assertEquals(form.working_directory_id, "wd-1-repo"); diff --git a/web/workspace/src/lib/workspace/sidebar/worker-launch.ts b/web/workspace/src/lib/workspace/sidebar/worker-launch.ts index 3049d9d8..54645fa4 100644 --- a/web/workspace/src/lib/workspace/sidebar/worker-launch.ts +++ b/web/workspace/src/lib/workspace/sidebar/worker-launch.ts @@ -32,9 +32,9 @@ export function defaultWorkerLaunchForm( ) ?? options?.runtimes.find((runtime) => runtime.can_spawn_worker) ?? options?.runtimes[0]; - const preferredProfile = - options?.profiles.find((candidate) => candidate.id === "builtin:coder") ?? - options?.profiles[0]; + const preferredProfile = options?.profiles.find((candidate) => + candidate.id === options.default_profile + ); const availableWorkingDirectories = options?.working_directories.filter((directory) => directory.status === "active" && @@ -60,7 +60,7 @@ export function defaultWorkerLaunchForm( return { runtime_id: current.runtime_id || preferredRuntime?.runtime_id || "", - display_name: current.display_name || "Coding Worker", + display_name: current.display_name || "Worker", profile: options?.profiles.some((candidate) => candidate.id === current.profile) ? current.profile diff --git a/web/workspace/src/lib/workspace/sidebar/workers.test.ts b/web/workspace/src/lib/workspace/sidebar/workers.test.ts index 5276a9f5..e8edc108 100644 --- a/web/workspace/src/lib/workspace/sidebar/workers.test.ts +++ b/web/workspace/src/lib/workspace/sidebar/workers.test.ts @@ -18,7 +18,6 @@ function worker(overrides: Partial): Worker { host_id: "host", display_name: "Worker 1", label: "Worker 1", - role: null, profile: null, singleton_key: null, tags: [], diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte index af09081e..ea98ba91 100644 --- a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte @@ -1259,11 +1259,8 @@
{worker.host_id}
-
Role / profile
-
- {worker.role ?? "unknown"} / {worker.profile ?? - "unknown"} -
+
Profile
+
{worker.profile ?? "unknown"}
Workspace
diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte index a93135e6..21053a6f 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte @@ -139,7 +139,7 @@ } function workerProfile(worker: Worker): string { - return worker.profile ?? worker.role ?? 'unknown'; + return worker.profile ?? 'unknown'; } function workerDirectory(worker: Worker): string { diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/new/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/new/+page.svelte index 2c2a4b87..a5c9b1ea 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/new/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/new/+page.svelte @@ -31,9 +31,9 @@ let optionsError = $state(null); let submitting = $state(false); let submitError = $state(null); - let displayName = $state('Coding Worker'); + let displayName = $state('Worker'); let runtimeId = $state(''); - let profile = $state('builtin:coder'); + let profile = $state(''); let initialText = $state(''); let workingDirectoryId = $state(''); let workingDirectoryRepositoryId = $state('');