From 006762f900162547ca7395fe4ed00db1b2508e46 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 26 Jul 2026 17:45:21 +0900 Subject: [PATCH] worker: separate display metadata --- crates/client/src/backend_runtime.rs | 6 + crates/worker-runtime/src/catalog.rs | 6 + crates/worker-runtime/src/http_server.rs | 2 + crates/worker-runtime/src/runtime.rs | 3 + crates/worker-runtime/src/worker_backend.rs | 1 + crates/workspace-server/src/hosts.rs | 169 ++++++++++++++++-- crates/workspace-server/src/server.rs | 21 ++- .../console/worker-console.ui.test.ts | 7 +- .../sidebar/WorkersNavSection.svelte | 4 +- .../src/lib/workspace/sidebar/types.ts | 3 + .../src/lib/workspace/sidebar/workers.test.ts | 5 +- .../w/[workspaceId]/workers/+page.svelte | 13 +- 12 files changed, 206 insertions(+), 34 deletions(-) diff --git a/crates/client/src/backend_runtime.rs b/crates/client/src/backend_runtime.rs index 3928e05f..40eb2259 100644 --- a/crates/client/src/backend_runtime.rs +++ b/crates/client/src/backend_runtime.rs @@ -141,11 +141,17 @@ pub struct BackendWorkerSummary { pub runtime_id: String, pub worker_id: String, pub host_id: String, + #[serde(default)] + pub display_name: String, pub label: String, #[serde(default)] pub role: Option, #[serde(default)] pub profile: Option, + #[serde(default)] + pub singleton_key: Option, + #[serde(default)] + pub tags: Vec, pub workspace: BackendWorkerWorkspaceSummary, pub state: String, #[serde(default)] diff --git a/crates/worker-runtime/src/catalog.rs b/crates/worker-runtime/src/catalog.rs index c611f587..868993a9 100644 --- a/crates/worker-runtime/src/catalog.rs +++ b/crates/worker-runtime/src/catalog.rs @@ -192,6 +192,8 @@ pub struct WorkspaceApiRef { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CreateWorkerRequest { pub profile: ProfileSelector, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, pub profile_source: ProfileSourceArchiveSource, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_bundle: Option, @@ -228,6 +230,8 @@ pub struct WorkerSummary { pub status: WorkerStatus, pub execution: WorkerExecutionStatus, pub profile: ProfileSelector, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, pub profile_source: ProfileSourceArchiveRef, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_bundle: Option, @@ -242,6 +246,8 @@ pub struct WorkerDetail { pub status: WorkerStatus, pub execution: WorkerExecutionStatus, pub profile: ProfileSelector, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_name: Option, pub profile_source: ProfileSourceArchiveRef, #[serde(default, skip_serializing_if = "Option::is_none")] pub config_bundle: Option, diff --git a/crates/worker-runtime/src/http_server.rs b/crates/worker-runtime/src/http_server.rs index f99c1800..411d423d 100644 --- a/crates/worker-runtime/src/http_server.rs +++ b/crates/worker-runtime/src/http_server.rs @@ -887,6 +887,7 @@ mod tests { let bundle = test_bundle(profile.clone()); CreateWorkerRequest { profile, + display_name: None, profile_source: crate::catalog::ProfileSourceArchiveSource::Http { location: crate::catalog::ProfileSourceArchiveHttpRef { url: "http://127.0.0.1/profile-source.tar".to_string(), @@ -1210,6 +1211,7 @@ mod ws_tests { let bundle = ws_test_bundle(ProfileSelector::RuntimeDefault); CreateWorkerRequest { profile: ProfileSelector::RuntimeDefault, + display_name: None, profile_source: crate::catalog::ProfileSourceArchiveSource::Http { location: crate::catalog::ProfileSourceArchiveHttpRef { url: "http://127.0.0.1/profile-source.tar".to_string(), diff --git a/crates/worker-runtime/src/runtime.rs b/crates/worker-runtime/src/runtime.rs index a4b1fe09..9d192eb2 100644 --- a/crates/worker-runtime/src/runtime.rs +++ b/crates/worker-runtime/src/runtime.rs @@ -1860,6 +1860,7 @@ impl WorkerRecord { .clone() .with_restore_dry_check(restore_dry_check), profile: self.request.profile.clone(), + display_name: self.request.display_name.clone(), profile_source: self.request.profile_source.reference(), config_bundle: self.request.config_bundle.clone(), last_event_id: self.last_event_id, @@ -1873,6 +1874,7 @@ impl WorkerRecord { status: self.status, execution: self.execution.clone(), profile: self.request.profile.clone(), + display_name: self.request.display_name.clone(), profile_source: self.request.profile_source.reference(), config_bundle: self.request.config_bundle.clone(), last_event_id: self.last_event_id, @@ -2008,6 +2010,7 @@ mod tests { let bundle = test_bundle_for_profile(profile.clone()); CreateWorkerRequest { profile, + display_name: None, profile_source: crate::catalog::ProfileSourceArchiveSource::Http { location: crate::catalog::ProfileSourceArchiveHttpRef { url: "http://127.0.0.1/profile-source.tar".to_string(), diff --git a/crates/worker-runtime/src/worker_backend.rs b/crates/worker-runtime/src/worker_backend.rs index a9b740fa..b70eda4c 100644 --- a/crates/worker-runtime/src/worker_backend.rs +++ b/crates/worker-runtime/src/worker_backend.rs @@ -1559,6 +1559,7 @@ mod tests { let bundle = test_bundle(); CreateWorkerRequest { profile: ProfileSelector::RuntimeDefault, + display_name: None, profile_source: crate::catalog::ProfileSourceArchiveSource::Embedded { archive: bundle.profile_source_archive.clone().unwrap(), }, diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index cf4d3d1d..2da783e0 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -230,9 +230,15 @@ pub struct WorkerSummary { pub runtime_id: String, pub worker_id: String, pub host_id: String, + /// Human-readable display name. This is not identity and may be duplicated. + 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)] + pub tags: Vec, pub workspace: WorkerWorkspaceSummary, pub state: String, pub last_seen_at: Option, @@ -1334,13 +1340,24 @@ impl EmbeddedWorkerRuntime { } fn map_worker_summary(&self, summary: worker_runtime::catalog::WorkerSummary) -> WorkerSummary { + let worker_id = summary.worker_ref.worker_id.to_string(); + let profile = embedded_profile_label(&summary.profile); + let display = worker_display_metadata( + &worker_id, + profile.as_deref(), + summary.display_name.as_deref(), + true, + ); WorkerSummary { runtime_id: self.runtime_id.clone(), - worker_id: summary.worker_ref.worker_id.to_string(), + worker_id, host_id: self.host_id.clone(), - label: safe_display_hint(&summary.worker_ref.worker_id.to_string()), - role: embedded_profile_label(&summary.profile), - profile: embedded_profile_label(&summary.profile), + display_name: display.display_name.clone(), + label: display.display_name, + role: profile.clone(), + profile, + singleton_key: display.singleton_key, + tags: display.tags, workspace: WorkerWorkspaceSummary { visibility: "backend_internal".to_string(), identity: "runtime_registry_worker".to_string(), @@ -1368,13 +1385,24 @@ impl EmbeddedWorkerRuntime { } fn map_worker_detail(&self, detail: EmbeddedWorkerDetail) -> WorkerSummary { + let worker_id = detail.worker_id.to_string(); + let profile = embedded_profile_label(&detail.profile); + let display = worker_display_metadata( + &worker_id, + profile.as_deref(), + detail.display_name.as_deref(), + true, + ); WorkerSummary { runtime_id: self.runtime_id.clone(), - worker_id: detail.worker_id.to_string(), + worker_id, host_id: self.host_id.clone(), - label: safe_display_hint(&detail.worker_id.to_string()), - role: embedded_profile_label(&detail.profile), - profile: embedded_profile_label(&detail.profile), + display_name: display.display_name.clone(), + label: display.display_name, + role: profile.clone(), + profile, + singleton_key: display.singleton_key, + tags: display.tags, workspace: WorkerWorkspaceSummary { visibility: "backend_internal".to_string(), identity: "runtime_registry_worker".to_string(), @@ -1579,9 +1607,9 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime { } if request.requested_worker_name.is_some() { diagnostics.push(diagnostic( - "embedded_worker_name_ignored", + "embedded_worker_name_display_only", DiagnosticSeverity::Info, - "Embedded Runtime v0 allocates opaque runtime-local worker ids; requested display names are not authority".to_string(), + "requested_worker_name is used only as display_name; embedded Runtime allocates opaque runtime-local worker ids".to_string(), )); } if matches!(request.acceptance, WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments } if expected_segments > 0) @@ -1615,6 +1643,7 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime { }; let create_request = CreateWorkerRequest { profile, + display_name: request.requested_worker_name.clone(), config_bundle: None, profile_source, initial_input: request.initial_input.clone(), @@ -2174,13 +2203,24 @@ impl RemoteWorkerRuntime { } fn map_worker_summary(&self, summary: worker_runtime::catalog::WorkerSummary) -> WorkerSummary { + let worker_id = summary.worker_ref.worker_id.to_string(); + let profile = embedded_profile_label(&summary.profile); + let display = worker_display_metadata( + &worker_id, + profile.as_deref(), + summary.display_name.as_deref(), + false, + ); WorkerSummary { runtime_id: self.runtime_id.clone(), - worker_id: summary.worker_ref.worker_id.to_string(), + worker_id, host_id: self.host_id.clone(), - label: safe_display_hint(&summary.worker_ref.worker_id.to_string()), - role: None, - profile: embedded_profile_label(&summary.profile), + display_name: display.display_name.clone(), + label: display.display_name, + role: profile.clone(), + profile, + singleton_key: display.singleton_key, + tags: display.tags, workspace: WorkerWorkspaceSummary { visibility: "remote_runtime".to_string(), identity: "runtime_registry_worker".to_string(), @@ -2208,13 +2248,24 @@ impl RemoteWorkerRuntime { } fn map_worker_detail(&self, detail: EmbeddedWorkerDetail) -> WorkerSummary { + let worker_id = detail.worker_id.to_string(); + let profile = embedded_profile_label(&detail.profile); + let display = worker_display_metadata( + &worker_id, + profile.as_deref(), + detail.display_name.as_deref(), + false, + ); WorkerSummary { runtime_id: self.runtime_id.clone(), - worker_id: detail.worker_id.to_string(), + worker_id, host_id: self.host_id.clone(), - label: safe_display_hint(&detail.worker_id.to_string()), - role: None, - profile: embedded_profile_label(&detail.profile), + display_name: display.display_name.clone(), + label: display.display_name, + role: profile.clone(), + profile, + singleton_key: display.singleton_key, + tags: display.tags, workspace: WorkerWorkspaceSummary { visibility: "remote_runtime".to_string(), identity: "runtime_registry_worker".to_string(), @@ -2473,6 +2524,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime { }; let create = CreateWorkerRequest { profile, + display_name: request.requested_worker_name.clone(), config_bundle: None, profile_source, initial_input: request.initial_input.clone(), @@ -3000,10 +3052,83 @@ fn ticket_role_profile_slug(role: &TicketWorkerRole) -> &'static str { fn embedded_profile_label(profile: &ProfileSelector) -> Option { Some(match profile { ProfileSelector::RuntimeDefault => "runtime_default".to_string(), - ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => safe_display_hint(name), + ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => { + if name.strip_prefix("builtin:").unwrap_or(name) == MEMORY_CONSOLIDATION_PROFILE { + MEMORY_CONSOLIDATION_PROFILE.to_string() + } else { + safe_display_hint(name) + } + } }) } +const MEMORY_CONSOLIDATION_PROFILE: &str = "memory-consolidation"; +const MEMORY_CONSOLIDATION_SINGLETON_KEY: &str = "workspace-memory-consolidation"; + +struct WorkerDisplayMetadata { + display_name: String, + singleton_key: Option, + tags: Vec, +} + +fn worker_display_metadata( + worker_id: &str, + profile_label: Option<&str>, + requested_display_name: Option<&str>, + internal: bool, +) -> WorkerDisplayMetadata { + if profile_label == Some(MEMORY_CONSOLIDATION_PROFILE) { + let mut tags = vec![ + "memory".to_string(), + "consolidation".to_string(), + "singleton".to_string(), + ]; + if internal { + tags.insert(0, "internal".to_string()); + } + return WorkerDisplayMetadata { + display_name: "Memory Consolidation".to_string(), + singleton_key: Some(MEMORY_CONSOLIDATION_SINGLETON_KEY.to_string()), + tags, + }; + } + let display_name = requested_display_name + .filter(|value| !value.trim().is_empty()) + .map(safe_display_hint) + .or_else(|| profile_label.map(profile_display_name)) + .unwrap_or_else(|| format!("Worker {worker_id}")); + let mut tags = Vec::new(); + if internal { + tags.push("internal".to_string()); + } + if let Some(profile_label) = profile_label { + tags.push(format!("profile:{profile_label}")); + } + WorkerDisplayMetadata { + display_name, + singleton_key: None, + tags, + } +} + +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(" "), + } +} + fn embedded_input_rejected( runtime_id: &str, worker_id: &str, @@ -3389,9 +3514,12 @@ pub fn placeholder_worker(host_id: impl Into) -> WorkerSummary { runtime_id: "placeholder".to_string(), worker_id: "worker-placeholder".to_string(), 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(), workspace: WorkerWorkspaceSummary { visibility: "none".to_string(), identity: "unsupported".to_string(), @@ -3720,9 +3848,12 @@ mod tests { runtime_id: runtime_id.to_string(), worker_id: worker_id.to_string(), 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(), workspace: WorkerWorkspaceSummary { visibility: "opaque".to_string(), identity: host_id.to_string(), diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 7bcf0e6f..d925734e 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -1514,6 +1514,7 @@ async fn scoped_memory_backend_operation( } const MEMORY_CONSOLIDATION_PROFILE: &str = "memory-consolidation"; +const MEMORY_CONSOLIDATION_SINGLETON_KEY: &str = "workspace-memory-consolidation"; const MEMORY_CONSOLIDATION_WORKER_SCAN_LIMIT: usize = 100; async fn scoped_memory_consolidation( @@ -1699,10 +1700,11 @@ fn try_reuse_memory_consolidation_worker( } fn is_memory_consolidation_worker(worker: &WorkerSummary) -> bool { - [worker.profile.as_deref(), worker.role.as_deref()] - .into_iter() - .flatten() - .any(|value| value == MEMORY_CONSOLIDATION_PROFILE) + 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) } fn memory_consolidation_input_content(candidate_count: usize, total_bytes: u64) -> String { @@ -5723,7 +5725,10 @@ fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary 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, + tags: Vec::new(), state: "missing".to_string(), last_seen_at: Some(record.updated_at.clone()), pinned: record.retention_state == "pinned", @@ -7395,6 +7400,13 @@ mod tests { .worker(EMBEDDED_WORKER_RUNTIME_ID, &worker_id) .unwrap(); assert_eq!(worker.state, "idle"); + assert_eq!(worker.display_name, "Memory Consolidation"); + assert_eq!( + worker.singleton_key.as_deref(), + Some(MEMORY_CONSOLIDATION_SINGLETON_KEY) + ); + assert!(worker.tags.iter().any(|tag| tag == "memory")); + assert!(worker.tags.iter().any(|tag| tag == "consolidation")); let second = match start_memory_staging_consolidation( api.clone(), @@ -7942,6 +7954,7 @@ mod tests { let bundle = runtime_test_bundle(); worker_runtime::catalog::CreateWorkerRequest { profile: worker_runtime::catalog::ProfileSelector::RuntimeDefault, + display_name: None, profile_source: worker_runtime::catalog::ProfileSourceArchiveSource::Http { location: worker_runtime::catalog::ProfileSourceArchiveHttpRef { url: "http://127.0.0.1/profile-source.tar".to_string(), diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index c47bd87d..c377bc91 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -114,13 +114,16 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async () assert( workersPage.includes("workerConsoleHref(worker, data.workspaceId)") && workersPage.includes('') && - workersPage.includes('class="icon-action"') && - workersPage.includes("Delete ${worker.label}"), + workersPage.includes("workerDisplayName = worker.display_name || worker.label") && + workersPage.includes("worker {worker.worker_id}") && + workersPage.includes("Delete ${workerDisplayName}"), "dedicated Workers page should expose a table, console link target, and icon actions per Worker", ); assert( workersNav.includes("href={`/w/${workspaceId}/workers`}") && workersNav.includes("filter(canShowWorkerInSidebar)") && + workersNav.includes("worker.display_name || worker.label") && + workersNav.includes("worker {worker.worker_id}") && !workersNav.includes('aria-disabled="true"'), "Workers sidebar should link to the Worker list page and omit registry-only Workers", ); diff --git a/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte b/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte index da6a801a..c9b4bd8a 100644 --- a/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte +++ b/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte @@ -105,11 +105,11 @@
  • - {worker.label} + {worker.display_name || worker.label} - - {worker.role ? `${worker.role} 路 ` : ''}{worker.state} 路 馃枼 {worker.host_id} + worker {worker.worker_id} 路 {worker.role ? `${worker.role} 路 ` : ''}{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 2e3a43f0..f25c2c1b 100644 --- a/web/workspace/src/lib/workspace/sidebar/types.ts +++ b/web/workspace/src/lib/workspace/sidebar/types.ts @@ -77,9 +77,12 @@ export type Worker = { runtime_id: string; worker_id: string; host_id: string; + display_name: string; label: string; role?: string | null; profile?: string | null; + singleton_key?: string | null; + tags: string[]; workspace: { visibility: string; identity: string }; state: string; pinned?: boolean; diff --git a/web/workspace/src/lib/workspace/sidebar/workers.test.ts b/web/workspace/src/lib/workspace/sidebar/workers.test.ts index 97e48627..5276a9f5 100644 --- a/web/workspace/src/lib/workspace/sidebar/workers.test.ts +++ b/web/workspace/src/lib/workspace/sidebar/workers.test.ts @@ -16,9 +16,12 @@ function worker(overrides: Partial): Worker { runtime_id: "arc", worker_id: "1", host_id: "host", - label: "worker-1", + display_name: "Worker 1", + label: "Worker 1", role: null, profile: null, + singleton_key: null, + tags: [], workspace: { visibility: "workspace", identity: "workspace" }, state: "running", pinned: false, diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte index fbf3e875..a93135e6 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte @@ -190,14 +190,15 @@ {@const cleanup = cleanupCandidate(worker)} {@const canDelete = cleanup && !cleanup.blocking_reason} {@const anyActionDisabled = actionsDisabled()} + {@const workerDisplayName = worker.display_name || worker.label}
  • @@ -205,12 +206,12 @@
    {#if canOpenWorkerConsole(worker)} - {worker.label} + {workerDisplayName} {:else} - {worker.label} + {workerDisplayName} {/if} - {worker.worker_id} + worker {worker.worker_id} {worker.runtime_id} {workerProfile(worker)}{worker.retention_state ?? 'normal'} {workerDirectory(worker)} -
    +