From 8695089fba346b8f10d55016b8328337e1c37445 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 11 Jul 2026 02:13:00 +0900 Subject: [PATCH] fix: sync backend worker registry projections --- .yoi/tickets/00001KX6BPY7M/item.md | 2 +- .yoi/tickets/00001KX6BPY7M/thread.md | 26 ++ crates/worker-runtime/src/catalog.rs | 4 + .../worker-runtime/src/working_directory.rs | 1 + crates/workspace-server/src/server.rs | 422 ++++++++++++++++-- crates/workspace-server/src/store.rs | 40 +- .../src/lib/workspace-sidebar/types.ts | 1 + 7 files changed, 468 insertions(+), 28 deletions(-) diff --git a/.yoi/tickets/00001KX6BPY7M/item.md b/.yoi/tickets/00001KX6BPY7M/item.md index 10c8e017..3d4d161c 100644 --- a/.yoi/tickets/00001KX6BPY7M/item.md +++ b/.yoi/tickets/00001KX6BPY7M/item.md @@ -2,7 +2,7 @@ title: 'Add Backend Worker/Workdir registry and link model' state: 'inprogress' created_at: '2026-07-10T15:53:02Z' -updated_at: '2026-07-10T16:38:10Z' +updated_at: '2026-07-10T17:12:46Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-10T16:10:57Z' diff --git a/.yoi/tickets/00001KX6BPY7M/thread.md b/.yoi/tickets/00001KX6BPY7M/thread.md index 13e2f35d..d87bd75f 100644 --- a/.yoi/tickets/00001KX6BPY7M/thread.md +++ b/.yoi/tickets/00001KX6BPY7M/thread.md @@ -169,4 +169,30 @@ Validation run: - nix build .#yoi --no-link +--- + + + +## Implementation report + +Follow-up fix for external review blockers. + +Fix summary: +- Browser Worker list now projects from Backend worker_registry rows after Runtime observation sync, preserving Backend-only stopped/archive rows and using worker_workdir_links as Workdir relationship authority. +- Runtime-scoped Worker spawn now writes pending Backend-managed Workdir registry rows, records spawned Workers, and links Workers to materialized or selected Workdirs. +- Worker stop now updates Backend Worker lifecycle and syncs linked Workdir records to present/removed/missing without deleting canonical rows. +- Runtime Workdir listing now syncs Runtime observations into Backend registry and projects typed management_kind (`backend_managed` / `runtime_unmanaged`) instead of returning raw Runtime summaries directly. +- Existing `pinned` Worker retention is preserved when Runtime observation/upsert sync supplies ordinary `normal` retention. +- Strengthened tests for archive/link projection, unmanaged Workdir projection, raw path redaction, removed Workdir preservation, and pinned retention preservation. + +Validation run: +- git diff --check +- cargo test -p yoi-workspace-server --lib +- cargo test -p worker-runtime --features ws-server,fs-store +- cargo check -p yoi +- cd web/workspace && deno task check && deno task test +- yoi ticket doctor +- nix build .#yoi --no-link + + --- diff --git a/crates/worker-runtime/src/catalog.rs b/crates/worker-runtime/src/catalog.rs index e0298a48..55d8210c 100644 --- a/crates/worker-runtime/src/catalog.rs +++ b/crates/worker-runtime/src/catalog.rs @@ -162,6 +162,10 @@ pub struct WorkingDirectorySummary { #[serde(default, skip_serializing_if = "Option::is_none")] pub cleanup_policy: Option, pub status: WorkingDirectoryStatusKind, + /// Backend projection metadata. Runtimes leave this absent; Workspace Browser + /// APIs fill it with `backend_managed` or `runtime_unmanaged`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub management_kind: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/worker-runtime/src/working_directory.rs b/crates/worker-runtime/src/working_directory.rs index a0718663..76f7c007 100644 --- a/crates/worker-runtime/src/working_directory.rs +++ b/crates/worker-runtime/src/working_directory.rs @@ -51,6 +51,7 @@ impl WorkingDirectory { cleanup_target: Some(self.cleanup_target.clone()), cleanup_policy: Some(self.cleanup_policy.clone()), status: self.status.clone(), + management_kind: None, } } } diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 58e28076..6050160c 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -24,10 +24,11 @@ use crate::config::{RemoteRuntimeConfigFile, WorkspaceBackendConfigFile, resolve use crate::hosts::{ ConfigBundleCheckResult, ConfigBundleSyncResult, DiagnosticSeverity, EmbeddedWorkerRuntime, HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, RuntimeDiagnostic, RuntimeRegistry, - RuntimeRegistryUnregisterResult, RuntimeSummary, WorkerInputRequest, WorkerInputResult, - WorkerLifecycleRequest, WorkerLifecycleResult, WorkerOperationState, - WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult, - WorkerSpawnWorkingDirectoryRequest, WorkerSummary, WorkerTranscriptProjection, + RuntimeRegistryUnregisterResult, RuntimeSummary, WorkerCapabilitySummary, + WorkerImplementationSummary, WorkerInputRequest, WorkerInputResult, WorkerLifecycleRequest, + WorkerLifecycleResult, WorkerOperationState, WorkerSpawnAcceptanceRequirement, + WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult, WorkerSpawnWorkingDirectoryRequest, + WorkerSummary, WorkerTranscriptProjection, WorkerWorkspaceSummary, }; use crate::identity::WorkspaceIdentity; use crate::observation::{ @@ -1186,18 +1187,11 @@ async fn scoped_list_runtime_working_directories( AxumPath(path): AxumPath, ) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; - let list = api - .runtime - .list_working_directories(&path.runtime_id) - .map_err(|err| err.into_error())?; + let (items, diagnostics) = runtime_working_directory_summaries(&api, &path.runtime_id)?; Ok(Json(BrowserWorkingDirectoryListResponse { workspace_id: api.config.workspace_id.clone(), - items: list - .items - .into_iter() - .map(|status| status.summary) - .collect(), - diagnostics: list.diagnostics, + items, + diagnostics, })) } @@ -2138,8 +2132,7 @@ async fn create_workspace_worker( code: "workspace_worker_create_missing_summary".to_string(), message: "Runtime completed worker creation without returning a Worker summary".to_string(), })?; - let worker_record = - record_worker_summary(&api, &worker, &display_name, Some(request.profile.clone()))?; + let worker_record = sync_worker_observation(&api, &worker)?; if let Some(workdir_id) = selected_working_directory_id.as_deref() { if api .store @@ -2278,10 +2271,47 @@ async fn create_runtime_worker( configured_working_directory_request(&api.config, working_directory) }) .transpose()?; + let prepared_workdir_id = if let Some(working_directory_request) = + request.resolved_working_directory_request.as_mut() + { + Some(upsert_pending_backend_workdir( + &api, + &runtime_id, + working_directory_request, + )?) + } else { + request + .resolved_working_directory + .as_ref() + .map(|claim| claim.working_directory_id.clone()) + }; let result = api .runtime .spawn_worker(&runtime_id, request) .map_err(|err| err.into_error())?; + if let Some(worker) = result.worker.as_ref() { + let record = sync_worker_observation(&api, worker)?; + if worker.working_directory.is_none() { + if let Some(workdir_id) = prepared_workdir_id.as_deref() { + if api + .store + .get_workdir_registry(&api.config.workspace_id, workdir_id)? + .is_some() + { + link_worker_to_workdir(&api, &record, workdir_id)?; + } + } + } + } else if let Some(workdir_id) = prepared_workdir_id.as_deref() { + if let Some(mut record) = api + .store + .get_workdir_registry(&api.config.workspace_id, workdir_id)? + { + record.materialization_status = "failed".to_string(); + record.updated_at = now_registry_timestamp(); + api.store.upsert_workdir_registry(&record)?; + } + } Ok(Json(result)) } @@ -2336,6 +2366,16 @@ async fn stop_runtime_worker( .runtime .stop_worker(&runtime_id, &worker_id, request) .map_err(|err| err.into_error())?; + let backend_id = backend_worker_id(&runtime_id, &worker_id); + if let Some(mut record) = api + .store + .get_worker_registry(&api.config.workspace_id, backend_id.as_str())? + { + record.lifecycle_state = "stopped".to_string(); + record.updated_at = now_registry_timestamp(); + api.store.upsert_worker_registry(&record)?; + sync_linked_workdir_after_worker_stop(&api, &runtime_id, &record)?; + } Ok(Json(result)) } @@ -2529,19 +2569,37 @@ async fn list_host_workers( fn workers_response(api: WorkspaceApi) -> ApiResult> { let limit = api.config.max_records.min(200); let runtime_workers = api.runtime.list_workers(limit); + let mut observed = std::collections::BTreeMap::new(); for worker in &runtime_workers.items { - let _ = record_worker_summary( - &api, - worker, - worker.worker_id.as_str(), - worker.profile.clone(), + let _ = sync_worker_observation(&api, worker); + observed.insert( + backend_worker_id(worker.runtime_id.as_str(), worker.worker_id.as_str()), + worker.clone(), ); } + let worker_records = api + .store + .list_worker_registry(&api.config.workspace_id, limit)?; + let workdir_records = api + .store + .list_workdir_registry(&api.config.workspace_id, 500)?; + let mut items = Vec::new(); + for record in worker_records { + let links = api + .store + .list_worker_workdir_links(&api.config.workspace_id, record.worker_id.as_str())?; + items.push(merge_worker_registry_projection( + observed.get(record.worker_id.as_str()), + &record, + links, + &workdir_records, + )); + } Ok(RuntimeListResponse { workspace_id: api.config.workspace_id, limit, - items: runtime_workers.items, - source: "backend_worker_registry_synced".to_string(), + items, + source: "backend_worker_registry".to_string(), diagnostics: runtime_workers.diagnostics, }) } @@ -3240,15 +3298,32 @@ fn working_directory_repository_options( } fn working_directory_summaries(api: &WorkspaceApi) -> ApiResult> { + let _ = sync_all_runtime_workdir_observations(api); let records = api .store - .list_managed_workdir_registry(&api.config.workspace_id, 200)?; + .list_workdir_registry(&api.config.workspace_id, 200)?; Ok(records .iter() .map(workdir_summary_from_record) .collect::>()) } +fn runtime_working_directory_summaries( + api: &WorkspaceApi, + runtime_id: &str, +) -> ApiResult<(Vec, Vec)> { + let diagnostics = sync_runtime_workdir_observations(api, runtime_id)?; + let records = api + .store + .list_workdir_registry(&api.config.workspace_id, 200)?; + let items = records + .iter() + .filter(|record| record.runtime_id == runtime_id) + .map(workdir_summary_from_record) + .collect::>(); + Ok((items, diagnostics)) +} + fn backend_worker_id(runtime_id: &str, runtime_worker_id: &str) -> String { format!("{runtime_id}/{runtime_worker_id}") } @@ -3297,7 +3372,7 @@ fn record_worker_summary( let worker_id = backend_worker_id(worker.runtime_id.as_str(), worker.worker_id.as_str()); let record = WorkerRegistryRecord { workspace_id: api.config.workspace_id.clone(), - worker_id, + worker_id: worker_id.clone(), runtime_id: worker.runtime_id.as_str().to_string(), runtime_worker_id: worker.worker_id.as_str().to_string(), display_name: display_name.to_string(), @@ -3316,9 +3391,216 @@ fn record_worker_summary( updated_at: timestamp, }; api.store.upsert_worker_registry(&record)?; + Ok(api + .store + .get_worker_registry(&api.config.workspace_id, worker_id.as_str())? + .unwrap_or(record)) +} + +fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary { + WorkerSummary { + worker_id: record.runtime_worker_id.clone(), + runtime_id: record.runtime_id.clone(), + host_id: "backend-registry".to_string(), + role: None, + label: record.display_name.clone(), + status: record.lifecycle_state.clone(), + state: record.lifecycle_state.clone(), + last_seen_at: Some(record.updated_at.clone()), + capabilities: WorkerCapabilitySummary { + can_accept_input: false, + can_stop: false, + can_spawn_followup: false, + }, + workspace: WorkerWorkspaceSummary { + visibility: "backend_registry".to_string(), + identity: record.workspace_id.clone(), + }, + profile: record.profile.clone(), + implementation: WorkerImplementationSummary { + kind: "backend_worker_registry".to_string(), + display_hint: "Archived Worker".to_string(), + }, + working_directory: None, + diagnostics: vec![RuntimeDiagnostic { + code: "backend_worker_registry_only".to_string(), + severity: DiagnosticSeverity::Info, + message: + "Worker is preserved in the Backend registry without a live Runtime observation" + .to_string(), + }], + } +} + +fn merge_worker_registry_projection( + live: Option<&WorkerSummary>, + record: &WorkerRegistryRecord, + links: Vec, + workdirs: &[WorkdirRegistryRecord], +) -> WorkerSummary { + let mut summary = live + .cloned() + .unwrap_or_else(|| worker_summary_from_registry(record)); + summary.label = record.display_name.clone(); + summary.status = record.lifecycle_state.clone(); + summary.state = record.lifecycle_state.clone(); + summary.profile = record.profile.clone(); + summary.working_directory = links.iter().find_map(|link| { + workdirs + .iter() + .find(|workdir| workdir.workdir_id == link.workdir_id) + .map(|workdir| workdir_summary_from_record(workdir)) + }); + summary +} + +fn sync_worker_observation( + api: &WorkspaceApi, + worker: &WorkerSummary, +) -> ApiResult { + let record = record_worker_summary(api, worker, worker.label.as_str(), worker.profile.clone())?; + if let Some(working_directory) = worker.working_directory.as_ref() { + let management_kind = api + .store + .get_workdir_registry( + &api.config.workspace_id, + &working_directory.working_directory_id, + )? + .map(|existing| existing.management_kind) + .unwrap_or_else(|| "runtime_unmanaged".to_string()); + let workdir_record = workdir_record_from_summary( + api, + worker.runtime_id.as_str(), + working_directory, + management_kind.as_str(), + ); + api.store.upsert_workdir_registry(&workdir_record)?; + link_worker_to_workdir(api, &record, &working_directory.working_directory_id)?; + } Ok(record) } +fn upsert_pending_backend_workdir( + api: &WorkspaceApi, + runtime_id: &str, + request: &mut WorkingDirectoryRequest, +) -> ApiResult { + let workdir_id = request + .backend_workdir_id + .clone() + .unwrap_or_else(|| next_backend_workdir_id(&request.repository.id)); + request.backend_workdir_id = Some(workdir_id.clone()); + let timestamp = now_registry_timestamp(); + api.store.upsert_workdir_registry(&WorkdirRegistryRecord { + workspace_id: api.config.workspace_id.clone(), + workdir_id: workdir_id.clone(), + runtime_id: runtime_id.to_string(), + repository_id: request.repository.id.clone(), + selector: request + .repository + .selector + .as_ref() + .map(|selector| selector.as_ref().to_string()), + resolved_commit: None, + materialization_status: "pending".to_string(), + cleanliness: "clean".to_string(), + management_kind: "backend_managed".to_string(), + created_at: timestamp.clone(), + updated_at: timestamp, + })?; + Ok(workdir_id) +} + +fn sync_runtime_workdir_observations( + api: &WorkspaceApi, + runtime_id: &str, +) -> ApiResult> { + let response = api + .runtime + .list_working_directories(runtime_id) + .map_err(|err| err.into_error())?; + let mut observed = std::collections::BTreeSet::new(); + for status in &response.items { + observed.insert(status.summary.working_directory_id.clone()); + let management_kind = api + .store + .get_workdir_registry( + &api.config.workspace_id, + &status.summary.working_directory_id, + )? + .map(|existing| existing.management_kind) + .unwrap_or_else(|| "runtime_unmanaged".to_string()); + let record = + workdir_record_from_summary(api, runtime_id, &status.summary, management_kind.as_str()); + api.store.upsert_workdir_registry(&record)?; + } + for mut record in api + .store + .list_workdir_registry(&api.config.workspace_id, 500)? + .into_iter() + .filter(|record| record.runtime_id == runtime_id && !observed.contains(&record.workdir_id)) + { + if record.materialization_status == "present" { + record.materialization_status = "missing".to_string(); + record.updated_at = now_registry_timestamp(); + api.store.upsert_workdir_registry(&record)?; + } + } + Ok(response.diagnostics) +} + +fn sync_all_runtime_workdir_observations(api: &WorkspaceApi) -> Vec { + let mut diagnostics = Vec::new(); + let runtimes = api.runtime.list_runtimes(api.config.max_records.min(200)); + for runtime in runtimes.items { + if runtime.capabilities.supports_worktrees { + match sync_runtime_workdir_observations(api, runtime.runtime_id.as_str()) { + Ok(mut runtime_diagnostics) => diagnostics.append(&mut runtime_diagnostics), + Err(err) => diagnostics.extend(err.diagnostics), + } + } + } + diagnostics +} + +fn sync_linked_workdir_after_worker_stop( + api: &WorkspaceApi, + runtime_id: &str, + worker_record: &WorkerRegistryRecord, +) -> ApiResult<()> { + let links = api + .store + .list_worker_workdir_links(&api.config.workspace_id, worker_record.worker_id.as_str())?; + for link in links { + let result = api + .runtime + .working_directory(runtime_id, link.workdir_id.as_str()) + .map_err(|err| err.into_error())?; + if let Some(status) = result.working_directory { + let management_kind = api + .store + .get_workdir_registry(&api.config.workspace_id, link.workdir_id.as_str())? + .map(|record| record.management_kind) + .unwrap_or_else(|| "runtime_unmanaged".to_string()); + let record = workdir_record_from_summary( + api, + runtime_id, + &status.summary, + management_kind.as_str(), + ); + api.store.upsert_workdir_registry(&record)?; + } else if let Some(mut record) = api + .store + .get_workdir_registry(&api.config.workspace_id, link.workdir_id.as_str())? + { + record.materialization_status = "missing".to_string(); + record.updated_at = now_registry_timestamp(); + api.store.upsert_workdir_registry(&record)?; + } + } + Ok(()) +} + fn workdir_record_from_summary( api: &WorkspaceApi, runtime_id: &str, @@ -3367,6 +3649,7 @@ fn workdir_summary_from_record(record: &WorkdirRegistryRecord) -> WorkingDirecto }), cleanup_policy: Some("manual_or_worker_stop".to_string()), status, + management_kind: Some(record.management_kind.clone()), } } @@ -3953,6 +4236,95 @@ mod tests { const TEST_REPOSITORY_ID: &str = "main"; const TEST_CREATED_AT: &str = "2026-06-23T06:43:28Z"; + #[test] + fn backend_worker_projection_preserves_archive_rows_links_and_redacts_paths() { + let worker = WorkerRegistryRecord { + workspace_id: "workspace-1".to_string(), + worker_id: "embedded/worker-1".to_string(), + runtime_id: "embedded".to_string(), + runtime_worker_id: "worker-1".to_string(), + display_name: "Archived Worker".to_string(), + profile: Some("builtin:coder".to_string()), + lifecycle_state: "stopped".to_string(), + retention_state: "pinned".to_string(), + transcript_ref: Some("runtime://embedded/workers/worker-1/transcript".to_string()), + session_ref: None, + summary_ref: None, + diagnostics_ref: None, + created_at: "1".to_string(), + updated_at: "2".to_string(), + }; + let workdir = WorkdirRegistryRecord { + workspace_id: "workspace-1".to_string(), + workdir_id: "backend-1-repo".to_string(), + runtime_id: "embedded".to_string(), + repository_id: "repo".to_string(), + selector: Some("develop".to_string()), + resolved_commit: Some("abcdef".to_string()), + materialization_status: "missing".to_string(), + cleanliness: "clean".to_string(), + management_kind: "backend_managed".to_string(), + created_at: "1".to_string(), + updated_at: "3".to_string(), + }; + let link = WorkerWorkdirLinkRecord { + workspace_id: "workspace-1".to_string(), + worker_id: worker.worker_id.clone(), + workdir_id: workdir.workdir_id.clone(), + role: "primary_cwd".to_string(), + linked_at: "4".to_string(), + unlinked_at: None, + }; + + let projected = merge_worker_registry_projection(None, &worker, vec![link], &[workdir]); + + assert_eq!(projected.status, "stopped"); + assert_eq!( + projected.working_directory.as_ref().unwrap().status, + WorkingDirectoryStatusKind::Removed + ); + assert_eq!( + projected + .working_directory + .as_ref() + .unwrap() + .management_kind + .as_deref(), + Some("backend_managed") + ); + let serialized = serde_json::to_string(&projected).unwrap(); + assert!(!serialized.contains("/tmp/")); + assert!(!serialized.contains("materialized_path")); + } + + #[test] + fn unmanaged_runtime_workdir_projection_is_typed_and_diagnostic_safe() { + let workdir = WorkdirRegistryRecord { + workspace_id: "workspace-1".to_string(), + workdir_id: "runtime-direct".to_string(), + runtime_id: "embedded".to_string(), + repository_id: "repo".to_string(), + selector: None, + resolved_commit: None, + materialization_status: "present".to_string(), + cleanliness: "unknown".to_string(), + management_kind: "runtime_unmanaged".to_string(), + created_at: "1".to_string(), + updated_at: "2".to_string(), + }; + + let projected = workdir_summary_from_record(&workdir); + + assert_eq!(projected.status, WorkingDirectoryStatusKind::Active); + assert_eq!( + projected.management_kind.as_deref(), + Some("runtime_unmanaged") + ); + let serialized = serde_json::to_string(&projected).unwrap(); + assert!(!serialized.contains("/tmp/")); + assert!(!serialized.contains("materialized_path")); + } + #[test] fn worker_profile_candidates_are_backend_published_and_mapped() { let candidates = worker_profile_candidates(); diff --git a/crates/workspace-server/src/store.rs b/crates/workspace-server/src/store.rs index 4dfb91f2..55c50e96 100644 --- a/crates/workspace-server/src/store.rs +++ b/crates/workspace-server/src/store.rs @@ -125,6 +125,11 @@ pub trait ControlPlaneStore: Send + Sync { workspace_id: &str, workdir_id: &str, ) -> Result>; + fn list_workdir_registry( + &self, + workspace_id: &str, + limit: usize, + ) -> Result>; fn list_managed_workdir_registry( &self, workspace_id: &str, @@ -234,7 +239,11 @@ impl ControlPlaneStore for SqliteWorkspaceStore { display_name = excluded.display_name, profile = excluded.profile, lifecycle_state = excluded.lifecycle_state, - retention_state = excluded.retention_state, + retention_state = CASE + WHEN worker_registry.retention_state = 'pinned' AND excluded.retention_state = 'normal' + THEN worker_registry.retention_state + ELSE excluded.retention_state + END, transcript_ref = excluded.transcript_ref, session_ref = excluded.session_ref, summary_ref = excluded.summary_ref, @@ -366,6 +375,25 @@ impl ControlPlaneStore for SqliteWorkspaceStore { }) } + fn list_workdir_registry( + &self, + workspace_id: &str, + limit: usize, + ) -> Result> { + self.with_conn(|conn| { + let sql = workdir_registry_select_sql( + "WHERE workspace_id = ?1 ORDER BY updated_at DESC LIMIT ?2", + ); + let mut stmt = conn.prepare(sql.as_str())?; + let rows = stmt.query_map( + params![workspace_id, limit as i64], + read_workdir_registry_record, + )?; + rows.collect::, _>>() + .map_err(Error::from) + }) + } + fn list_managed_workdir_registry( &self, workspace_id: &str, @@ -1221,6 +1249,14 @@ mod tests { updated_at: "2".to_string(), }; store.upsert_worker_registry(&worker).unwrap(); + let mut runtime_sync_worker = worker.clone(); + runtime_sync_worker.lifecycle_state = "running".to_string(); + runtime_sync_worker.retention_state = "normal".to_string(); + runtime_sync_worker.updated_at = "5".to_string(); + store.upsert_worker_registry(&runtime_sync_worker).unwrap(); + let mut expected_worker = worker.clone(); + expected_worker.lifecycle_state = "running".to_string(); + expected_worker.updated_at = "5".to_string(); let workdir = WorkdirRegistryRecord { workspace_id: "local-dev".to_string(), @@ -1251,7 +1287,7 @@ mod tests { store .get_worker_registry_by_runtime("local-dev", "embedded", "browser-1") .unwrap(), - Some(worker.clone()) + Some(expected_worker.clone()) ); assert_eq!( store diff --git a/web/workspace/src/lib/workspace-sidebar/types.ts b/web/workspace/src/lib/workspace-sidebar/types.ts index e8379c87..9e926f52 100644 --- a/web/workspace/src/lib/workspace-sidebar/types.ts +++ b/web/workspace/src/lib/workspace-sidebar/types.ts @@ -124,6 +124,7 @@ export type WorkingDirectorySummary = { resolved_commit: string; resolved_tree?: string | null; status: string; + management_kind?: "backend_managed" | "runtime_unmanaged" | string | null; cleanup_policy: string; cleanup_target: { kind: string;