worker: separate display metadata

This commit is contained in:
Keisuke Hirata 2026-07-26 17:45:21 +09:00
parent ff905d4a22
commit 006762f900
No known key found for this signature in database
12 changed files with 206 additions and 34 deletions

View File

@ -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<String>,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub singleton_key: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
pub workspace: BackendWorkerWorkspaceSummary,
pub state: String,
#[serde(default)]

View File

@ -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<String>,
pub profile_source: ProfileSourceArchiveSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_bundle: Option<ConfigBundleRef>,
@ -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<String>,
pub profile_source: ProfileSourceArchiveRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_bundle: Option<ConfigBundleRef>,
@ -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<String>,
pub profile_source: ProfileSourceArchiveRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_bundle: Option<ConfigBundleRef>,

View File

@ -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(),

View File

@ -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(),

View File

@ -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(),
},

View File

@ -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<String>,
pub profile: Option<String>,
pub singleton_key: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
pub workspace: WorkerWorkspaceSummary,
pub state: String,
pub last_seen_at: Option<String>,
@ -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<String> {
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<String>,
tags: Vec<String>,
}
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::<String>(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" "),
}
}
fn embedded_input_rejected(
runtime_id: &str,
worker_id: &str,
@ -3389,9 +3514,12 @@ pub fn placeholder_worker(host_id: impl Into<String>) -> 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(),

View File

@ -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(),

View File

@ -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('<table class="workers-table">') &&
workersPage.includes('class="icon-action"') &&
workersPage.includes("Delete ${worker.label}"),
workersPage.includes("workerDisplayName = worker.display_name || worker.label") &&
workersPage.includes("worker <code>{worker.worker_id}</code>") &&
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",
);

View File

@ -105,11 +105,11 @@
<li>
<a href={href} class="nav-item worker-nav-item" class:active={currentPath === href} aria-current={currentPath === href ? 'page' : undefined}>
<span class="worker-title-row">
<span class="item-title">{worker.label}</span>
<span class="item-title">{worker.display_name || worker.label}</span>
<span class="worker-task-title">-</span>
</span>
<span class="item-meta">
{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)}` : ''}
</span>
</a>

View File

@ -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;

View File

@ -16,9 +16,12 @@ function worker(overrides: Partial<Worker>): 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,

View File

@ -190,14 +190,15 @@
{@const cleanup = cleanupCandidate(worker)}
{@const canDelete = cleanup && !cleanup.blocking_reason}
{@const anyActionDisabled = actionsDisabled()}
{@const workerDisplayName = worker.display_name || worker.label}
<tr>
<td>
{#if canOpenWorkerConsole(worker)}
<a class="worker-title-link" href={workerConsoleHref(worker, data.workspaceId)}><strong>{worker.label}</strong></a>
<a class="worker-title-link" href={workerConsoleHref(worker, data.workspaceId)}><strong>{workerDisplayName}</strong></a>
{:else}
<strong>{worker.label}</strong>
<strong>{workerDisplayName}</strong>
{/if}
<small><code>{worker.worker_id}</code></small>
<small>worker <code>{worker.worker_id}</code></small>
</td>
<td><code>{worker.runtime_id}</code></td>
<td>{workerProfile(worker)}</td>
@ -205,12 +206,12 @@
<td><span class="pill {worker.pinned ? 'success' : 'muted'}">{worker.retention_state ?? 'normal'}</span></td>
<td>{workerDirectory(worker)}</td>
<td>
<div class="worker-actions" aria-label={`Actions for ${worker.label}`}>
<div class="worker-actions" aria-label={`Actions for ${workerDisplayName}`}>
<button
class="icon-action"
type="button"
disabled={anyActionDisabled}
aria-label={worker.pinned ? `Unpin ${worker.label}` : `Pin ${worker.label}`}
aria-label={worker.pinned ? `Unpin ${workerDisplayName}` : `Pin ${workerDisplayName}`}
title={worker.pinned ? 'Unpin' : 'Pin'}
onclick={() => setPinned(worker, !worker.pinned)}
>
@ -227,7 +228,7 @@
class="icon-action danger"
type="button"
disabled={!canDelete || anyActionDisabled}
aria-label={`Delete ${worker.label}`}
aria-label={`Delete ${workerDisplayName}`}
title={cleanup.blocking_reason ?? cleanup.reason}
onclick={() => deleteWorker(worker, cleanup)}
>