diff --git a/crates/protocol/src/subscription.rs b/crates/protocol/src/subscription.rs index 3f017b640..c26ba6eff 100644 --- a/crates/protocol/src/subscription.rs +++ b/crates/protocol/src/subscription.rs @@ -564,6 +564,7 @@ pub enum SubscriptionWorkerState { /// `Unavailable` deliberately does not imply `Stopped`: it means the last observed /// lifecycle state is retained while the Runtime observation source is unavailable. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum SubscriptionWorkerAvailability { diff --git a/crates/server-api/src/lib.rs b/crates/server-api/src/lib.rs index 883707b29..0023404ed 100644 --- a/crates/server-api/src/lib.rs +++ b/crates/server-api/src/lib.rs @@ -7931,6 +7931,9 @@ pub struct WorkerSummary { #[serde(default)] pub tags: Vec, pub workspace: WorkerWorkspaceSummary, + /// Freshness of the Runtime observation used to construct `worker_state`. + /// `Unavailable` means no current controller snapshot is being asserted. + pub availability: protocol::subscription::SubscriptionWorkerAvailability, /// Runtime catalog lifecycle compatibility state. Live foreground state, when /// available, is carried separately in `worker_state`. pub state: String, diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index 31950aba9..a9aaa978f 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -260,6 +260,9 @@ pub struct InternalWorkerSummary { #[serde(default)] pub tags: Vec, pub workspace: InternalWorkerWorkspaceSummary, + /// Freshness of the Runtime observation carried by `worker_state`. + #[serde(default)] + pub availability: protocol::subscription::SubscriptionWorkerAvailability, /// Runtime catalog lifecycle compatibility state. pub state: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -371,6 +374,7 @@ pub(crate) fn workspace_worker_summary( identity: summary.workspace.identity, workspace_id: summary.workspace.workspace_id, }, + availability: summary.availability, state: summary.state, worker_state: summary.worker_state, last_seen_at: summary.last_seen_at, @@ -2218,6 +2222,8 @@ impl EmbeddedWorkerRuntime { identity: "runtime_registry_worker".to_string(), workspace_id: summary.workspace_id.clone(), }, + availability: + protocol::subscription::SubscriptionWorkerAvailability::Observed, state: embedded_worker_state_label( summary.status, summary.execution_metadata_available, @@ -2265,6 +2271,8 @@ impl EmbeddedWorkerRuntime { identity: "runtime_registry_worker".to_string(), workspace_id: detail.workspace_id.clone(), }, + availability: + protocol::subscription::SubscriptionWorkerAvailability::Observed, state: embedded_worker_state_label(detail.status, detail.execution_metadata_available) .to_string(), worker_state: detail.worker_state.clone(), @@ -4056,6 +4064,8 @@ impl RemoteWorkerRuntime { identity: "runtime_registry_worker".to_string(), workspace_id: summary.workspace_id.clone(), }, + availability: + protocol::subscription::SubscriptionWorkerAvailability::Observed, state: embedded_worker_state_label( summary.status, summary.execution_metadata_available, @@ -4101,6 +4111,8 @@ impl RemoteWorkerRuntime { identity: "runtime_registry_worker".to_string(), workspace_id: detail.workspace_id.clone(), }, + availability: + protocol::subscription::SubscriptionWorkerAvailability::Observed, state: embedded_worker_state_label(detail.status, detail.execution_metadata_available) .to_string(), worker_state: detail.worker_state.clone(), @@ -5867,6 +5879,7 @@ pub fn placeholder_worker(host_id: impl Into) -> InternalWorkerSummary { identity: "unsupported".to_string(), workspace_id: None, }, + availability: protocol::subscription::SubscriptionWorkerAvailability::Unavailable, state: "unsupported".to_string(), worker_state: None, last_seen_at: None, @@ -6509,6 +6522,8 @@ mod tests { identity: host_id.to_string(), workspace_id: None, }, + availability: + protocol::subscription::SubscriptionWorkerAvailability::Observed, state: "available".to_string(), worker_state: None, last_seen_at: None, diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index de383f98f..c689ef820 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -24059,6 +24059,7 @@ fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> InternalWorker identity: record.workspace_id.clone(), workspace_id: Some(record.workspace_id.clone()), }, + availability: protocol::subscription::SubscriptionWorkerAvailability::Unavailable, profile: record.profile.clone(), implementation: InternalWorkerImplementationSummary { kind: "backend_worker_registry".to_string(), @@ -24082,6 +24083,7 @@ fn worker_summary_from_projection( let mut summary = worker_summary_from_registry(&projection.registry); let observed = observation.availability == protocol::subscription::SubscriptionWorkerAvailability::Observed; + summary.availability = observation.availability; summary.state = if observed { match observation.worker.state { protocol::subscription::SubscriptionWorkerState::Idle => "idle", @@ -24093,7 +24095,9 @@ fn worker_summary_from_projection( "unavailable" } .to_string(); - summary.worker_state = observation.worker.worker_state.clone(); + summary.worker_state = observed + .then(|| observation.worker.worker_state.clone()) + .flatten(); summary.last_seen_at = Some(observation.observed_at.clone()); summary.capabilities.can_stop = observed && observation.worker.state != protocol::subscription::SubscriptionWorkerState::Stopped; @@ -25798,9 +25802,79 @@ mod tests { use crate::store::{ AccountRecord, ApiTokenRecord, BrowserSessionRecord, MemoryDocumentRecord, MemoryStagingRecord, ObjectiveRecord, ObjectiveResourceRecord, ObjectiveTicketLinkRecord, - SqliteWorkspaceStore, UserRecord, WorkspaceRecord, WorkspaceRuntimeBinding, + SqliteWorkspaceStore, UserRecord, WorkerRegistryObservationRecord, + WorkerRegistryProjectionRecord, WorkerRegistryRecord, WorkspaceRecord, + WorkspaceRuntimeBinding, }; + #[test] + fn worker_projection_does_not_expose_unavailable_snapshot_as_current() { + let availability = + protocol::subscription::SubscriptionWorkerAvailability::Unavailable; + let worker_ref = RuntimeWorkerRef::new("runtime-a", "worker-a"); + let projection = WorkerRegistryProjectionRecord { + registry: WorkerRegistryRecord { + workspace_id: TEST_WORKSPACE_ID.to_string(), + worker: worker_ref, + display_name: "Worker A".to_string(), + profile: Some("builtin:coder".to_string()), + retention_state: "normal".to_string(), + transcript_ref: None, + session_ref: None, + summary_ref: None, + diagnostics_ref: None, + created_at: "2026-09-24T00:00:00Z".to_string(), + updated_at: "2026-09-24T00:00:00Z".to_string(), + }, + resource_key: Some("W-1".to_string()), + observation: Some(WorkerRegistryObservationRecord { + worker: protocol::subscription::SubscriptionWorker { + worker_id: protocol::subscription::SubscriptionWorkerId::new("worker-a") + .unwrap(), + runtime_id: Some("runtime-a".to_string()), + resource_key: Some("W-1".to_string()), + availability, + subject_revision: 4, + worker_state: Some(protocol::WorkerStateSnapshot::initial()), + state: protocol::subscription::SubscriptionWorkerState::Idle, + has_running_internal_workers: false, + workspace_id: Some(TEST_WORKSPACE_ID.to_string()), + display_name: Some("Worker A".to_string()), + profile: Some("builtin:coder".to_string()), + workdir_attachments: Vec::new(), + }, + availability, + connection_generation: 2, + subject_revision: 4, + snapshot_revision: 7, + projection_revision: 9, + observed_at: "2026-09-24T00:00:01Z".to_string(), + }), + }; + + let unavailable = worker_summary_from_projection(&projection).unwrap(); + assert_eq!(unavailable.availability, availability); + assert_eq!(unavailable.state, "unavailable"); + assert_eq!(unavailable.worker_state, None); + assert!(!unavailable.capabilities.can_stop); + + let mut observed_stopped = projection; + let observation = observed_stopped.observation.as_mut().unwrap(); + observation.availability = + protocol::subscription::SubscriptionWorkerAvailability::Observed; + observation.worker.availability = + protocol::subscription::SubscriptionWorkerAvailability::Observed; + observation.worker.state = protocol::subscription::SubscriptionWorkerState::Stopped; + observation.worker.worker_state = None; + let observed_stopped = worker_summary_from_projection(&observed_stopped).unwrap(); + assert_eq!( + observed_stopped.availability, + protocol::subscription::SubscriptionWorkerAvailability::Observed + ); + assert_eq!(observed_stopped.state, "stopped"); + assert_eq!(observed_stopped.worker_state, None); + } + #[tokio::test] async fn external_provider_session_reuses_operation_contract_and_read_only_capabilities() { let (sender, mut commands) = tokio::sync::mpsc::channel(1); @@ -26717,6 +26791,8 @@ mod tests { identity: TEST_WORKSPACE_ID.to_string(), workspace_id: Some(TEST_WORKSPACE_ID.to_string()), }, + availability: + protocol::subscription::SubscriptionWorkerAvailability::Observed, state: "idle".to_string(), worker_state: None, last_seen_at: None, @@ -38514,6 +38590,8 @@ mod tests { identity: "workspace-test".to_string(), workspace_id: Some(TEST_WORKSPACE_ID.to_string()), }, + availability: + protocol::subscription::SubscriptionWorkerAvailability::Observed, state: "idle".to_string(), worker_state: None, last_seen_at: None, @@ -38611,6 +38689,8 @@ mod tests { identity: "workspace-test".to_string(), workspace_id: Some(TEST_WORKSPACE_ID.to_string()), }, + availability: + protocol::subscription::SubscriptionWorkerAvailability::Observed, state: "idle".to_string(), worker_state: None, last_seen_at: None, diff --git a/openapi/server-api.json b/openapi/server-api.json index fb2c77fa3..b2ad8ca1e 100644 --- a/openapi/server-api.json +++ b/openapi/server-api.json @@ -7367,6 +7367,10 @@ "additionalProperties": false, "description": "Workspace-authoritative Worker projection.\n\n`resource_key` is required here even though Runtime-internal Worker summaries\ndo not carry one. The Workspace Server must resolve it from Workspace\nauthority before constructing this response.", "properties": { + "availability": { + "$ref": "#/components/schemas/SubscriptionWorkerAvailability", + "description": "Freshness of the Runtime observation used to construct `worker_state`.\n`Unavailable` means no current controller snapshot is being asserted." + }, "capabilities": { "$ref": "#/components/schemas/WorkerCapabilitySummary" }, @@ -7466,6 +7470,7 @@ "host_id", "label", "workspace", + "availability", "state", "implementation", "capabilities", @@ -8605,6 +8610,14 @@ ], "type": "object" }, + "SubscriptionWorkerAvailability": { + "description": "Whether the Workspace Server has a current Runtime observation for a Worker.\n\n`Unavailable` deliberately does not imply `Stopped`: it means the last observed\nlifecycle state is retained while the Runtime observation source is unavailable.", + "enum": [ + "observed", + "unavailable" + ], + "type": "string" + }, "Ticket": { "properties": { "artifacts": { @@ -11722,6 +11735,10 @@ "WorkerSummary": { "description": "Workspace-authoritative Worker projection.\n\n`resource_key` is required here even though Runtime-internal Worker summaries\ndo not carry one. The Workspace Server must resolve it from Workspace\nauthority before constructing this response.", "properties": { + "availability": { + "$ref": "#/components/schemas/SubscriptionWorkerAvailability", + "description": "Freshness of the Runtime observation used to construct `worker_state`.\n`Unavailable` means no current controller snapshot is being asserted." + }, "capabilities": { "$ref": "#/components/schemas/WorkerCapabilitySummary" }, @@ -11818,6 +11835,7 @@ "host_id", "label", "workspace", + "availability", "state", "implementation", "capabilities" @@ -13901,7 +13919,7 @@ "info": { "title": "Yoi Server API", "version": "0.1.0", - "x-yoi-source-digest": "sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c" + "x-yoi-source-digest": "sha256:a92b7346166ec2316878ef38b0426090cb4bdd0d5b96cae3014bef35c60df02a" }, "openapi": "3.1.0", "paths": { diff --git a/web/workspace/src/lib/generated/repository-api.ts b/web/workspace/src/lib/generated/repository-api.ts index e6a5d5591..72ec21104 100644 --- a/web/workspace/src/lib/generated/repository-api.ts +++ b/web/workspace/src/lib/generated/repository-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-repository-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c) +// Input contract: 0.1.0 (sha256:a92b7346166ec2316878ef38b0426090cb4bdd0d5b96cae3014bef35c60df02a) // Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output // Output: web/workspace/src/lib/generated/repository-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check @@ -55,6 +55,8 @@ export type RepositorySshHostTrust = { created_at: string; current_revision: num export type RepositorySummary = { default_selector?: string | null; diagnostics?: Array | null; git?: GitRepositorySummary | null; kind: string; observed_at?: string | null; observed_status: RepositoryObservedStatus; provider: string; record_authority: string; repository_key: string; source: RepositorySource; source_fingerprint: string; source_revision: number; }; +export type SubscriptionWorkerAvailability = "observed" | "unavailable"; + export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; }; export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; }; @@ -69,7 +71,7 @@ export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyS export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; }; -export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; +export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; }; diff --git a/web/workspace/src/lib/generated/runtime-api.ts b/web/workspace/src/lib/generated/runtime-api.ts index feeaadae9..791ad8d01 100644 --- a/web/workspace/src/lib/generated/runtime-api.ts +++ b/web/workspace/src/lib/generated/runtime-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-runtime-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c) +// Input contract: 0.1.0 (sha256:a92b7346166ec2316878ef38b0426090cb4bdd0d5b96cae3014bef35c60df02a) // Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/runtime-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check @@ -105,7 +105,7 @@ export type RuntimeWorkerLifecycleResult = { diagnostics?: Array; ru export type RuntimeWorkerListResponse = { diagnostics?: Array; items: Array; limit: number; source: string; workspace_id: string; }; -export type RuntimeWorkerShowResponse = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; updated_at: string; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; +export type RuntimeWorkerShowResponse = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; updated_at: string; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; export type RuntimeWorkerSpawnAcceptanceEvidence = { detail: string; kind: string; }; @@ -125,6 +125,8 @@ export type RuntimeWorkerSummary = { capabilities: WorkerCapabilitySummary; diag export type RuntimeWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; }; +export type SubscriptionWorkerAvailability = "observed" | "unavailable"; + export type UpdateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; }; export type UploadedFileAvailability = "available" | "unavailable" | "integrity_failed"; @@ -159,7 +161,7 @@ export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyS export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; }; -export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; +export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; }; diff --git a/web/workspace/src/lib/generated/worker-launch-api.ts b/web/workspace/src/lib/generated/worker-launch-api.ts index 79b39f7b6..08442458b 100644 --- a/web/workspace/src/lib/generated/worker-launch-api.ts +++ b/web/workspace/src/lib/generated/worker-launch-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-worker-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c) +// Input contract: 0.1.0 (sha256:a92b7346166ec2316878ef38b0426090cb4bdd0d5b96cae3014bef35c60df02a) // Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/worker-launch-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check @@ -35,6 +35,8 @@ export type RuntimeWorkingDirectorySummary = { cleanliness?: string | null; clea export type Segment = { content: string; kind: "text"; } | { chars: number; content: string; id: number; kind: "paste"; lines: number; } | { artifact: PasteArtifactRef; kind: "paste_artifact"; } | { file: UploadedFileRef; kind: "uploaded_file"; } | { kind: "file_ref"; path: string; } | { kind: "flow"; selector: string; } | { kind: "unknown"; }; +export type SubscriptionWorkerAvailability = "observed" | "unavailable"; + export type UploadedFileAvailability = "available" | "unavailable" | "integrity_failed"; export type UploadedFileRef = { artifact_id: string; availability: UploadedFileAvailability; byte_len: number; created_at_ms: number; file_name: string; media_type: string; sha256: string; source_entry_id?: string | null; }; @@ -63,7 +65,7 @@ export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyS export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; }; -export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; +export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array; workdir_attachments?: Array; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; }; export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; }; diff --git a/web/workspace/src/lib/workspace/api/workers.test.ts b/web/workspace/src/lib/workspace/api/workers.test.ts index de930e42d..2667a0e63 100644 --- a/web/workspace/src/lib/workspace/api/workers.test.ts +++ b/web/workspace/src/lib/workspace/api/workers.test.ts @@ -31,6 +31,7 @@ import { parseBrowserWorkspaceOrchestratorResponse, parseCreateWorkspaceWorkerRequest, parseWorkerLaunchOptionsResponse, + parseWorkerSummary, } from "./workers.ts"; const worker = { @@ -62,6 +63,39 @@ const worker = { diagnostics: [], }; +Deno.test("Worker summary parser enforces observation freshness", () => { + const unavailable = { + ...worker, + resource_key: "W-1", + availability: "unavailable", + state: "unavailable", + }; + const parsed = parseWorkerSummary(unavailable); + assertEquals(parsed.availability, "unavailable"); + assertEquals(parsed.worker_state, undefined); + + assertThrows( + () => + parseWorkerSummary({ + ...unavailable, + worker_state: { + last_command_id: 0, + state: { kind: "idle" }, + }, + }), + Error, + "worker_state must be absent", + ); + + const observedStopped = parseWorkerSummary({ + ...unavailable, + availability: "observed", + state: "stopped", + }); + assertEquals(observedStopped.availability, "observed"); + assertEquals(observedStopped.state, "stopped"); +}); + Deno.test("Worker launch options parser accepts the generated wire shape", () => { const parsed = parseWorkerLaunchOptionsResponse({ workspace_id: "workspace-a", diff --git a/web/workspace/src/lib/workspace/api/workers.ts b/web/workspace/src/lib/workspace/api/workers.ts index 66d66371a..26dcefe8c 100644 --- a/web/workspace/src/lib/workspace/api/workers.ts +++ b/web/workspace/src/lib/workspace/api/workers.ts @@ -343,6 +343,10 @@ export function parseWorkerSummary( label = "Worker summary", ): ParsedWorkerSummary { const item = record(value, label); + const availability = string(item.availability, `${label}.availability`); + if (availability !== "observed" && availability !== "unavailable") { + throw new Error(`${label}.availability is invalid`); + } exact( item, [ @@ -356,6 +360,7 @@ export function parseWorkerSummary( "singleton_key", "tags", "workspace", + "availability", "state", "worker_state", "last_seen_at", @@ -368,6 +373,16 @@ export function parseWorkerSummary( ], label, ); + const workerState = optional( + item.worker_state, + `${label}.worker_state`, + workerStateSnapshot, + ); + if (availability === "unavailable" && workerState != null) { + throw new Error( + `${label}.worker_state must be absent when availability is unavailable`, + ); + } return { runtime_id: string(item.runtime_id, `${label}.runtime_id`), worker_id: string(item.worker_id, `${label}.worker_id`), @@ -387,12 +402,9 @@ export function parseWorkerSummary( ? [] : array(item.tags, `${label}.tags`, string), workspace: workspaceSummary(item.workspace, `${label}.workspace`), + availability, state: string(item.state, `${label}.state`), - worker_state: optional( - item.worker_state, - `${label}.worker_state`, - workerStateSnapshot, - ), + worker_state: workerState, last_seen_at: optional(item.last_seen_at, `${label}.last_seen_at`, string), pinned: item.pinned === undefined ? false diff --git a/web/workspace/src/lib/workspace/sidebar/worker-subscription.ts b/web/workspace/src/lib/workspace/sidebar/worker-subscription.ts index 915b469de..beb398bb6 100644 --- a/web/workspace/src/lib/workspace/sidebar/worker-subscription.ts +++ b/web/workspace/src/lib/workspace/sidebar/worker-subscription.ts @@ -146,6 +146,7 @@ function projectWorker( visibility: "workspace", identity: "runtime_subscription_worker", }, + availability: worker.availability, state: liveWorkerState(worker), worker_state: worker.worker_state, pinned: false,