diff --git a/crates/client/src/backend_runtime.rs b/crates/client/src/backend_runtime.rs index d3a0fed54..7b9fbd42a 100644 --- a/crates/client/src/backend_runtime.rs +++ b/crates/client/src/backend_runtime.rs @@ -819,7 +819,8 @@ mod tests { "profiles": [{ "id": "builtin:default", "label": "Default", - "description": "" + "description": "", + "feature_connections": { "subjektiv": false } }], "repositories": [], "working_directories": [], @@ -883,6 +884,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }; diff --git a/crates/client/src/workspace_product.rs b/crates/client/src/workspace_product.rs index cda30d257..c65d1b4a2 100644 --- a/crates/client/src/workspace_product.rs +++ b/crates/client/src/workspace_product.rs @@ -532,6 +532,7 @@ impl BackendWorkspaceProductClient { content: format!("Please handle intake for Ticket {ticket_id}."), }], workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }; let workspace_id = self.workspace_id.clone(); diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 49cbf34cb..e043de416 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -175,6 +175,11 @@ pub struct WorkerStateSnapshot { schemars(range(min = 0, max = 9_007_199_254_740_991_u64)) )] pub last_command_id: u64, + /// Most recent Submit request whose controller Run has returned and whose + /// post-Run state transition has been published. This is a teardown fence, + /// not a successful-result claim; cancelled and failed Runs may also finish. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_finished_submission_request_id: Option, pub state: WorkerState, } @@ -182,6 +187,7 @@ impl WorkerStateSnapshot { pub fn initial() -> Self { Self { last_command_id: 0, + last_finished_submission_request_id: None, state: WorkerState::Idle, } } @@ -209,6 +215,7 @@ impl From for WorkerStateSnapshot { }; Self { last_command_id: 0, + last_finished_submission_request_id: None, state, } } @@ -1037,10 +1044,11 @@ pub enum SessionContentPart { #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] pub struct SessionToolAttachment { + /// Stable, bounded identity derived from the owning Session entry, attachment + /// position, and immutable image bytes. It is a locator, not a bearer secret. + pub attachment_id: String, pub media_type: String, - /// Base64-encoded durable attachment body. Public snapshots preserve the - /// committed multimodal value instead of replacing it with placeholder text. - pub data_base64: String, + pub byte_len: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1925,6 +1933,7 @@ mod tests { fn worker_state_snapshot_wire_shape_has_one_authoritative_state() { let snapshot = WorkerStateSnapshot { last_command_id: 7, + last_finished_submission_request_id: Some("request-7".to_string()), state: WorkerState::Busy(WorkerBusyState::Run(WorkerRunState::Running)), }; let value = serde_json::to_value(&snapshot).unwrap(); @@ -1932,6 +1941,7 @@ mod tests { value, serde_json::json!({ "last_command_id": 7, + "last_finished_submission_request_id": "request-7", "state": { "kind": "busy", "state": { "kind": "run", "state": "running" } @@ -2648,6 +2658,7 @@ mod tests { let event = Event::WorkerState { snapshot: WorkerStateSnapshot { last_command_id: 9, + last_finished_submission_request_id: None, state: WorkerState::Busy(WorkerBusyState::Run(WorkerRunState::Running)), }, }; @@ -2669,6 +2680,7 @@ mod tests { snapshot: WorkerStateSnapshot { last_command_id: 9, state: WorkerState::Busy(WorkerBusyState::Run(WorkerRunState::Running)), + .. } } )); diff --git a/crates/runtime-api/src/lib.rs b/crates/runtime-api/src/lib.rs index 729deb98d..eaf3135ef 100644 --- a/crates/runtime-api/src/lib.rs +++ b/crates/runtime-api/src/lib.rs @@ -1085,6 +1085,8 @@ pub const RUNTIME_ROUTE_WORKER_PROTOCOL_WS: &str = "/v1/workers/{worker_id}/prot pub const RUNTIME_ROUTE_WORKER_ATTACHMENTS: &str = "/v1/workers/{worker_id}/attachments"; pub const RUNTIME_ROUTE_WORKER_ATTACHMENT: &str = "/v1/workers/{worker_id}/attachments/{artifact_id}"; +pub const RUNTIME_ROUTE_WORKER_SESSION_ATTACHMENT: &str = + "/v1/workers/{worker_id}/sessions/{session_id}/attachments/{attachment_id}"; /// Intentionally out-of-contract Runtime routes. This inventory keeps manual paths visible rather /// than letting them be mistaken for management-contract omissions. @@ -1189,6 +1191,11 @@ pub const REMAINING_RUNTIME_ROUTES: &[RemainingRuntimeRoute] = &[ path: RUNTIME_ROUTE_WORKER_ATTACHMENT, reason: "Binary attachment transport", }, + RemainingRuntimeRoute { + method: "GET", + path: RUNTIME_ROUTE_WORKER_SESSION_ATTACHMENT, + reason: "Session image attachment transport", + }, ]; #[cfg(test)] @@ -1478,7 +1485,7 @@ mod tests { #[test] fn remaining_route_inventory_does_not_overlap_contract() { - assert_eq!(REMAINING_RUNTIME_ROUTES.len(), 20); + assert_eq!(REMAINING_RUNTIME_ROUTES.len(), 21); for remaining in REMAINING_RUNTIME_ROUTES { assert!(!RuntimeApiMetadata::OPERATIONS.iter().any(|operation| { format!("{:?}", operation.method).eq_ignore_ascii_case(remaining.method) diff --git a/crates/server-api/src/lib.rs b/crates/server-api/src/lib.rs index 0e911803e..0df95a96f 100644 --- a/crates/server-api/src/lib.rs +++ b/crates/server-api/src/lib.rs @@ -249,7 +249,6 @@ impl_openapi_schema!( SubjektivMemoryQueryResponse, SubjektivMemoryReadResponse, SubjektivMemoryListRevisionsResponse, - SubjektivSubjectWorkerStartRequest, SubjektivStageCandidateRequest, SubjektivStageCandidateResponse, SubjektivRecordSessionRequest, @@ -1234,21 +1233,6 @@ pub trait ServerApi { #[path] workspace_id: String, #[path] subject_id: String, ) -> Result; - #[post( - "/api/w/{workspace_id}/subjektiv/subjects/{subject_id}/worker", - status = 200, - error_status = 400, - additional_error_statuses = [401, 403, 404, 409, 500, 502, 503], - bearer_auth = true, - browser_auth = true, - )] - async fn subjektiv_subject_worker_start( - &self, - #[extension] context: ServerRequestContext, - #[path] workspace_id: String, - #[path] subject_id: String, - #[body] request: SubjektivSubjectWorkerStartRequest, - ) -> Result; #[post( "/api/w/{workspace_id}/subjektiv/staging", status = 200, @@ -4780,12 +4764,6 @@ pub struct SubjektivMemoryRevisionsQuery { pub cursor: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct SubjektivSubjectWorkerStartRequest { - pub worker: CreateWorkspaceWorkerRequest, -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SubjektivStageCandidateRequest { @@ -9489,6 +9467,14 @@ pub struct WorkerLaunchProfileCandidate { pub id: String, pub label: String, pub description: String, + pub feature_connections: WorkerLaunchFeatureConnectionOptions, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkerLaunchFeatureConnectionOptions { + pub subjektiv: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -9520,6 +9506,22 @@ pub struct CreateWorkspaceWorkerTicketAssignmentRequest { pub operation_id: String, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct WorkspaceWorkerFeatureConnectionsRequest { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional = nullable))] + pub subjektiv: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct SubjektivWorkerConnectionRequest { + pub subject_id: String, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] @@ -9537,6 +9539,9 @@ pub struct CreateWorkspaceWorkerRequest { pub initial_submit: Vec, #[serde(default)] pub workdir_attachments: Vec, + /// Explicit product Feature connections. Backend validation turns these into + /// trusted host attachments; Runtime and Profile inputs cannot self-attest them. + pub feature_connections: WorkspaceWorkerFeatureConnectionsRequest, /// Backend idempotency key used only for authenticated Worker-owned spawn/control. #[serde(default)] pub control_operation_id: Option, @@ -10705,11 +10710,14 @@ pub fn worker_launch_api_typescript() -> String { RuntimeWorkerWorkdirAttachmentSummary::decl(&config), WorkerLaunchWorkerSummary::decl(&config), WorkerLaunchRuntimeOption::decl(&config), + WorkerLaunchFeatureConnectionOptions::decl(&config), WorkerLaunchProfileCandidate::decl(&config), WorkingDirectoryRepositoryOption::decl(&config), WorkerLaunchOptionsResponse::decl(&config), BrowserWorkerWorkingDirectorySelection::decl(&config), CreateWorkspaceWorkerTicketAssignmentRequest::decl(&config), + SubjektivWorkerConnectionRequest::decl(&config), + WorkspaceWorkerFeatureConnectionsRequest::decl(&config), CreateWorkspaceWorkerRequest::decl(&config), BrowserCreateWorkerResponse::decl(&config), BrowserWorkspaceOrchestratorResponse::decl(&config), @@ -11750,6 +11758,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }) .unwrap(); @@ -11763,6 +11772,7 @@ mod tests { "ticket_assignment": null, "initial_submit": [], "workdir_attachments": [], + "feature_connections": {}, "control_operation_id": null, }) ); diff --git a/crates/session-store/src/lib.rs b/crates/session-store/src/lib.rs index ac44c0be4..50bfc4697 100644 --- a/crates/session-store/src/lib.rs +++ b/crates/session-store/src/lib.rs @@ -110,9 +110,12 @@ pub use public_snapshot::{ DEFAULT_RETAINED_HISTORY_MAX_ENTRIES, DEFAULT_RETAINED_HISTORY_MAX_RESPONSE_BYTES, DEFAULT_RETAINED_HISTORY_MAX_SCAN_BYTES, DEFAULT_RETAINED_HISTORY_MAX_SEGMENTS, DEFAULT_RETAINED_HISTORY_PAGE_TURNS, DEFAULT_RETAINED_SNAPSHOT_MAX_BYTES, - MAX_RETAINED_HISTORY_PAGE_TURNS, RetainedHistoryReadError, RetainedHistoryReadLimits, - RetainedSessionIdentity, RetainedSessionSnapshot, RetainedSnapshotReadError, - project_session_snapshot, read_retained_session_history_page, read_retained_session_snapshot, + MAX_RETAINED_HISTORY_PAGE_TURNS, RetainedAttachmentReadError, RetainedHistoryReadError, + RetainedHistoryReadLimits, RetainedSessionAttachment, RetainedSessionIdentity, + RetainedSessionSnapshot, RetainedSnapshotReadError, project_session_snapshot, + read_retained_session_attachment, read_retained_session_history_page, + read_retained_session_snapshot, read_session_attachment_from_entries, + session_tool_attachment_id, }; pub use segment::{ SegmentStartState, append_entry, append_system_item, classify_logged_history_entry, diff --git a/crates/session-store/src/public_snapshot.rs b/crates/session-store/src/public_snapshot.rs index 9fdadc373..bc7659c12 100644 --- a/crates/session-store/src/public_snapshot.rs +++ b/crates/session-store/src/public_snapshot.rs @@ -1,7 +1,4 @@ -use base64::{ - Engine as _, - engine::general_purpose::{STANDARD as BASE64, URL_SAFE_NO_PAD}, -}; +use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use protocol::{ Segment, SessionContentPart, SessionConversationTurn, SessionEntryProvenance, SessionHistoryLineageBoundary, SessionHistoryPage, SessionMessageRole, SessionSnapshot, @@ -54,6 +51,32 @@ pub enum RetainedSnapshotReadError { SnapshotTooLarge, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RetainedSessionAttachment { + pub media_type: String, + pub data: Vec, +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum RetainedAttachmentReadError { + #[error("retained worker state is unavailable")] + RetentionMissing, + #[error("retained worker state has no active session pointer")] + ActivePointerMissing, + #[error("retained session identity does not match")] + SessionMismatch, + #[error("session attachment was not found")] + NotFound, + #[error("retained session requires migration")] + MigrationRequired, + #[error("retained session log is corrupt")] + CorruptLog, + #[error("retained session storage is unavailable")] + StorageUnavailable, + #[error("retained session attachment exceeds a resource limit")] + ResourceLimit, +} + pub const DEFAULT_RETAINED_HISTORY_PAGE_TURNS: usize = 5; pub const MAX_RETAINED_HISTORY_PAGE_TURNS: usize = 5; pub const DEFAULT_RETAINED_HISTORY_MAX_SCAN_BYTES: u64 = 64 * 1024 * 1024; @@ -175,7 +198,9 @@ pub fn read_retained_session_snapshot( .ok_or(RetainedSnapshotReadError::ActivePointerMissing)?; let store = WorkerSessionStore::open_read_only(aggregate_root.join("session")) .map_err(map_store_error)?; - if store.segment_log_len(segment_id).map_err(map_store_error)? > max_bytes { + if store.segment_log_len(segment_id).map_err(map_store_error)? + > DEFAULT_RETAINED_HISTORY_MAX_SCAN_BYTES + { return Err(RetainedSnapshotReadError::SnapshotTooLarge); } let entries = store @@ -199,6 +224,153 @@ pub fn read_retained_session_snapshot( }) } +/// Resolve one immutable image body from the active retained Session. The +/// caller supplies only public identities; storage paths never cross this API. +pub fn read_retained_session_attachment( + aggregate_root: &Path, + worker_name: &str, + expected_session_id: &str, + attachment_id: &str, + max_scan_bytes: u64, + max_attachment_bytes: u64, +) -> Result { + let aggregate = WorkerAggregateStore::open_read_only(aggregate_root, worker_name) + .map_err(map_attachment_worker_store_error)?; + let metadata = aggregate + .read_read_only() + .map_err(map_attachment_worker_store_error)? + .ok_or(RetainedAttachmentReadError::RetentionMissing)?; + let active = metadata + .active + .ok_or(RetainedAttachmentReadError::ActivePointerMissing)?; + if active.session_id.to_string() != expected_session_id { + return Err(RetainedAttachmentReadError::SessionMismatch); + } + let segment_id = active + .segment_id + .ok_or(RetainedAttachmentReadError::ActivePointerMissing)?; + let store = WorkerSessionStore::open_read_only(aggregate_root.join("session")) + .map_err(map_attachment_store_error)?; + let limits = RetainedHistoryReadLimits { + max_scan_bytes, + ..RetainedHistoryReadLimits::default() + }; + let (lineage, _, mut scanned_bytes, mut scanned_entries) = + load_adopted_lineage(&store, active.session_id, segment_id, limits) + .map_err(map_history_attachment_error)?; + for segment in &lineage { + let adopted_end = locate_adopted_segment_end( + &store, + active.session_id, + segment, + limits, + &mut scanned_bytes, + &mut scanned_entries, + ) + .map_err(map_history_attachment_error)?; + let mut reader = store + .open_retained_segment_reader(active.session_id, segment.segment_id, adopted_end) + .map_err(map_attachment_store_error)?; + loop { + let remaining = limits.max_scan_bytes.saturating_sub(scanned_bytes); + let (record, bytes) = reader + .previous_record(remaining) + .map_err(map_attachment_store_error)?; + scanned_bytes = scanned_bytes + .checked_add(bytes) + .ok_or(RetainedAttachmentReadError::ResourceLimit)?; + let Some(record) = record else { + break; + }; + scanned_entries = scanned_entries + .checked_add(persisted_entry_units(std::slice::from_ref(&record.entry))) + .ok_or(RetainedAttachmentReadError::ResourceLimit)?; + if scanned_entries > limits.max_entries { + return Err(RetainedAttachmentReadError::ResourceLimit); + } + if let Some(result) = + session_attachment_from_record(&record.entry, attachment_id, max_attachment_bytes) + { + return result; + } + } + } + Err(RetainedAttachmentReadError::NotFound) +} + +pub fn read_session_attachment_from_entries( + entries: &[LogEntry], + attachment_id: &str, + max_attachment_bytes: u64, +) -> Result { + for record in entries { + if let Some(result) = + session_attachment_from_record(record, attachment_id, max_attachment_bytes) + { + return result; + } + } + Err(RetainedAttachmentReadError::NotFound) +} + +fn session_attachment_from_record( + record: &LogEntry, + attachment_id: &str, + max_attachment_bytes: u64, +) -> Option> { + let mut found = None; + visit_record_history(record, &mut |entry| { + if found.is_none() { + found = attachment_from_entry(entry, attachment_id, max_attachment_bytes); + } + }); + found +} + +fn visit_record_history(record: &LogEntry, visit: &mut impl FnMut(&LoggedHistoryEntry)) { + match record { + LogEntry::AnnotatedSegmentStart { history, .. } + | LogEntry::AnnotatedUserInput { history, .. } => { + for entry in history { + visit(entry); + } + } + LogEntry::AnnotatedAssistantItem { entry, .. } + | LogEntry::AnnotatedToolResult { entry, .. } => visit(entry), + _ => {} + } +} + +fn attachment_from_entry( + entry: &LoggedHistoryEntry, + requested_id: &str, + max_attachment_bytes: u64, +) -> Option> { + let LoggedItem::ToolResult { attachments, .. } = &entry.item else { + return None; + }; + for (attachment_index, attachment) in attachments.iter().enumerate() { + let crate::logged_item::LoggedAttachment::Image { mime_type, data } = attachment; + if session_tool_attachment_id( + &entry.metadata.entry_id.0, + attachment_index, + mime_type, + data, + ) != requested_id + { + continue; + } + if data.len() as u64 > max_attachment_bytes { + return Some(Err(RetainedAttachmentReadError::ResourceLimit)); + } + return Some(Ok(RetainedSessionAttachment { + media_type: mime_type.clone(), + data: data.clone(), + })); + } + None +} + /// Read one bounded backward page from the retained active Segment's adopted /// lineage without creating, migrating, restoring, or rewriting persisted state. /// @@ -935,6 +1107,54 @@ fn map_history_store_error(error: StoreError) -> RetainedHistoryReadError { } } +fn map_history_attachment_error(error: RetainedHistoryReadError) -> RetainedAttachmentReadError { + match error { + RetainedHistoryReadError::RetentionMissing => RetainedAttachmentReadError::RetentionMissing, + RetainedHistoryReadError::ActivePointerMissing => { + RetainedAttachmentReadError::ActivePointerMissing + } + RetainedHistoryReadError::MigrationRequired => { + RetainedAttachmentReadError::MigrationRequired + } + RetainedHistoryReadError::CorruptLog | RetainedHistoryReadError::InvalidCursor => { + RetainedAttachmentReadError::CorruptLog + } + RetainedHistoryReadError::StorageUnavailable => { + RetainedAttachmentReadError::StorageUnavailable + } + RetainedHistoryReadError::ResourceLimit => RetainedAttachmentReadError::ResourceLimit, + } +} + +fn map_attachment_worker_store_error(error: WorkerStoreError) -> RetainedAttachmentReadError { + match error { + WorkerStoreError::Io(error) if error.kind() == std::io::ErrorKind::NotFound => { + RetainedAttachmentReadError::RetentionMissing + } + WorkerStoreError::Serde(_) | WorkerStoreError::InvalidWorkerName(_) => { + RetainedAttachmentReadError::CorruptLog + } + WorkerStoreError::Io(_) => RetainedAttachmentReadError::StorageUnavailable, + } +} + +fn map_attachment_store_error(error: StoreError) -> RetainedAttachmentReadError { + match error { + StoreError::Io(error) if error.kind() == std::io::ErrorKind::NotFound => { + RetainedAttachmentReadError::RetentionMissing + } + StoreError::Corrupt { message, .. } if message.contains("requires migration") => { + RetainedAttachmentReadError::MigrationRequired + } + StoreError::ReadLimitExceeded => RetainedAttachmentReadError::ResourceLimit, + StoreError::Io(_) => RetainedAttachmentReadError::StorageUnavailable, + StoreError::Serde(_) | StoreError::Corrupt { .. } | StoreError::NotFound(_) => { + RetainedAttachmentReadError::CorruptLog + } + _ => RetainedAttachmentReadError::CorruptLog, + } +} + fn map_worker_store_error(error: WorkerStoreError) -> RetainedSnapshotReadError { match error { WorkerStoreError::Io(error) if error.kind() == std::io::ErrorKind::NotFound => { @@ -1028,7 +1248,7 @@ pub(crate) fn project_session_snapshot_for_segment( } => extend_history(&mut entries, history, Some(segments), *ts), LogEntry::AnnotatedAssistantItem { ts, entry } | LogEntry::AnnotatedToolResult { ts, entry } => { - if let Some(data) = project_item(&entry.item) { + if let Some(data) = project_item(&entry.metadata.entry_id.0, &entry.item) { entries.push(history_entry(entry, *ts, data)); } } @@ -1188,7 +1408,7 @@ fn extend_history( segments: input_segments.cloned().unwrap_or_default(), } } else { - let Some(data) = project_item(&entry.item) else { + let Some(data) = project_item(&entry.metadata.entry_id.0, &entry.item) else { continue; }; data @@ -1306,7 +1526,25 @@ fn provenance(origin: &LoggedSessionHistoryOrigin) -> SessionEntryProvenance { } } -fn project_item(item: &LoggedItem) -> Option { +pub fn session_tool_attachment_id( + entry_id: &str, + attachment_index: usize, + media_type: &str, + data: &[u8], +) -> String { + let mut hash = Sha256::new(); + hash.update(b"yoi-session-tool-attachment-v1\0"); + hash.update((entry_id.len() as u64).to_be_bytes()); + hash.update(entry_id.as_bytes()); + hash.update((attachment_index as u64).to_be_bytes()); + hash.update((media_type.len() as u64).to_be_bytes()); + hash.update(media_type.as_bytes()); + hash.update((data.len() as u64).to_be_bytes()); + hash.update(data); + URL_SAFE_NO_PAD.encode(hash.finalize()) +} + +fn project_item(entry_id: &str, item: &LoggedItem) -> Option { match item { LoggedItem::Message { role, content } => { let role = match role { @@ -1355,11 +1593,18 @@ fn project_item(item: &LoggedItem) -> Option { is_error: *is_error, attachments: attachments .iter() - .map(|attachment| match attachment { + .enumerate() + .map(|(attachment_index, attachment)| match attachment { crate::logged_item::LoggedAttachment::Image { mime_type, data } => { SessionToolAttachment { + attachment_id: session_tool_attachment_id( + entry_id, + attachment_index, + mime_type, + data, + ), media_type: mime_type.clone(), - data_base64: BASE64.encode(data), + byte_len: data.len() as u64, } } }) @@ -3307,6 +3552,206 @@ mod tests { )); } + #[test] + fn retained_attachment_resolves_from_an_adopted_compaction_ancestor() { + let worker_name = "worker-ancestor-image"; + let session_id = crate::new_session_id(); + let source_segment = crate::new_segment_id(); + let active_segment = crate::new_segment_id(); + let image_entry = LoggedHistoryEntry { + item: LoggedItem::ToolResult { + call_id: "call-ancestor-image".into(), + summary: "ancestor screenshot".into(), + content: None, + attachments: vec![crate::logged_item::LoggedAttachment::Image { + mime_type: "image/png".into(), + data: vec![7, 8, 9], + }], + disposition: agen::tool::ToolResultDisposition::Success, + is_error: false, + }, + metadata: LoggedSessionHistoryMetadata { + entry_id: LoggedSessionHistoryEntryId("ancestor-image".into()), + origin: LoggedSessionHistoryOrigin::LegacyUnknown, + derivation: None, + }, + }; + let mut source_log = vec![segment_start(session_id, Vec::new(), None, None)]; + source_log.push(LogEntry::AnnotatedUserInput { + ts: 2, + segments: vec![Segment::Text { + content: "show image".into(), + }], + history: vec![user_message(1)], + extensions: Vec::new(), + }); + source_log.push(LogEntry::AnnotatedToolResult { + ts: 3, + entry: image_entry, + }); + source_log.push(LogEntry::TurnEnd { + ts: 4, + turn_count: 1, + }); + let active_log = vec![segment_start( + session_id, + Vec::new(), + None, + Some(SegmentOrigin { + segment_id: source_segment, + at_turn_index: 1, + }), + )]; + let (_root, aggregate_root) = persist_history_fixture( + worker_name, + session_id, + active_segment, + vec![(source_segment, source_log), (active_segment, active_log)], + ); + + let page = read_retained_session_history_page( + &aggregate_root, + worker_name, + None, + None, + RetainedHistoryReadLimits::default(), + ) + .unwrap(); + let attachment = page.turns[0] + .entries + .iter() + .find_map(|entry| match &entry.data { + SessionSnapshotEntryData::ToolResult { attachments, .. } => attachments.first(), + _ => None, + }) + .expect("ancestor attachment reference"); + let body = read_retained_session_attachment( + &aggregate_root, + worker_name, + &session_id.to_string(), + &attachment.attachment_id, + DEFAULT_RETAINED_HISTORY_MAX_SCAN_BYTES, + 10 * 1024 * 1024, + ) + .unwrap(); + assert_eq!(body.media_type, "image/png"); + assert_eq!(body.data, vec![7, 8, 9]); + } + + #[test] + fn large_legacy_inline_images_project_as_bounded_refs_and_fetch_from_retained_storage() { + let worker_name = "worker-images"; + let session_id = crate::new_session_id(); + let segment_id = crate::new_segment_id(); + // Approximate the decoded byte sizes of the four W-296 screenshots. + let sizes = [3_475_194_usize, 3_459_051, 5_735_340, 5_703_924]; + let attachments = sizes + .iter() + .enumerate() + .map( + |(index, size)| crate::logged_item::LoggedAttachment::Image { + mime_type: "image/png".into(), + data: vec![index as u8 + 1; *size], + }, + ) + .collect(); + let tool_result = LoggedHistoryEntry { + item: LoggedItem::ToolResult { + call_id: "call-images".into(), + summary: "four screenshots".into(), + content: None, + attachments, + disposition: agen::tool::ToolResultDisposition::Success, + is_error: false, + }, + metadata: LoggedSessionHistoryMetadata { + entry_id: LoggedSessionHistoryEntryId("tool-images".into()), + origin: LoggedSessionHistoryOrigin::LegacyUnknown, + derivation: None, + }, + }; + let live = project_current_session_snapshot(&[LogEntry::AnnotatedToolResult { + ts: 2, + entry: tool_result.clone(), + }]); + let log = vec![LogEntry::AnnotatedSegmentStart { + ts: 1, + session_id, + system_prompt: None, + config: RequestConfig::default(), + history: vec![tool_result], + forked_from: None, + compacted_from: None, + }]; + let (_root, aggregate_root) = + persist_history_fixture(worker_name, session_id, segment_id, vec![(segment_id, log)]); + + let retained = read_retained_session_snapshot( + &aggregate_root, + worker_name, + DEFAULT_RETAINED_SNAPSHOT_MAX_BYTES, + ) + .unwrap(); + let SessionSnapshotEntryData::ToolResult { attachments, .. } = + &retained.snapshot.entries[0].data + else { + panic!("expected tool result"); + }; + assert_eq!(attachments.len(), 4); + let SessionSnapshotEntryData::ToolResult { + attachments: live_attachments, + .. + } = &live.entries[0].data + else { + panic!("expected live tool result"); + }; + assert_eq!(live_attachments, attachments); + let snapshot_json = serde_json::to_vec(&retained.snapshot).unwrap(); + assert!(snapshot_json.len() < 16 * 1024); + assert!(!String::from_utf8_lossy(&snapshot_json).contains("data_base64")); + + for (index, attachment) in attachments.iter().enumerate() { + assert_eq!(attachment.byte_len, sizes[index] as u64); + assert_eq!(attachment.attachment_id.len(), 43); + let body = read_retained_session_attachment( + &aggregate_root, + worker_name, + &session_id.to_string(), + &attachment.attachment_id, + DEFAULT_RETAINED_HISTORY_MAX_SCAN_BYTES, + 10 * 1024 * 1024, + ) + .unwrap(); + assert_eq!(body.media_type, "image/png"); + assert_eq!(body.data.len(), sizes[index]); + assert!(body.data.iter().all(|byte| *byte == index as u8 + 1)); + } + assert_eq!( + read_retained_session_attachment( + &aggregate_root, + worker_name, + &crate::new_session_id().to_string(), + &attachments[0].attachment_id, + DEFAULT_RETAINED_HISTORY_MAX_SCAN_BYTES, + 10 * 1024 * 1024, + ) + .unwrap_err(), + RetainedAttachmentReadError::SessionMismatch + ); + assert_eq!( + read_retained_session_attachment( + &aggregate_root, + worker_name, + &session_id.to_string(), + "../../metadata.json", + DEFAULT_RETAINED_HISTORY_MAX_SCAN_BYTES, + 10 * 1024 * 1024, + ) + .unwrap_err(), + RetainedAttachmentReadError::NotFound + ); + } + #[test] fn retained_reads_do_not_create_missing_aggregate() { let root = tempfile::tempdir().unwrap(); diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index a303f9e38..1a1f7f156 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -3761,6 +3761,7 @@ mod completion_flow_tests { protocol::WorkerRunState::Running, )), last_command_id: 0, + last_finished_submission_request_id: None, }; app.handle_worker_event(Event::WorkerState { snapshot: running.clone(), @@ -3780,6 +3781,7 @@ mod completion_flow_tests { protocol::WorkerRunState::Running, )), last_command_id: 2, + last_finished_submission_request_id: Some("request-old".to_string()), }; app.handle_worker_event(Event::WorkerState { snapshot: running.clone(), @@ -3789,6 +3791,7 @@ mod completion_flow_tests { let fresh_idle = WorkerStateSnapshot { state: protocol::WorkerState::Idle, last_command_id: 0, + last_finished_submission_request_id: None, }; app.handle_worker_event(Event::WorkerState { snapshot: fresh_idle.clone(), @@ -3800,6 +3803,7 @@ mod completion_flow_tests { protocol::WorkerRunState::Paused, )), last_command_id: 3, + last_finished_submission_request_id: None, }; app.handle_worker_event(Event::CommandAcknowledged { acknowledgement: protocol::WorkerCommandAcknowledgement { diff --git a/crates/tui/src/backend_spawn.rs b/crates/tui/src/backend_spawn.rs index 40d4da58c..db7a22a35 100644 --- a/crates/tui/src/backend_spawn.rs +++ b/crates/tui/src/backend_spawn.rs @@ -167,6 +167,7 @@ fn request_from_selection(selection: Selection) -> BackendCreateWorkerRequest { initial_submit: Vec::new(), workdir_attachments: Vec::new(), ticket_assignment: None, + feature_connections: Default::default(), control_operation_id: None, } } @@ -431,11 +432,13 @@ mod tests { id: "builtin:default".to_string(), label: "Default".to_string(), description: String::new(), + feature_connections: Default::default(), }, BackendWorkerLaunchProfileCandidate { id: "builtin:coder".to_string(), label: "Coder".to_string(), description: "Ticket implementation".to_string(), + feature_connections: Default::default(), }, ], default_profile: Some("builtin:coder".to_string()), diff --git a/crates/tui/src/backend_worker_picker.rs b/crates/tui/src/backend_worker_picker.rs index 0b67597cb..962912b63 100644 --- a/crates/tui/src/backend_worker_picker.rs +++ b/crates/tui/src/backend_worker_picker.rs @@ -579,6 +579,7 @@ mod tests { protocol::WorkerRunState::Running, )), last_command_id: 0, + last_finished_submission_request_id: None, }), last_seen_at: None, pinned: false, @@ -869,6 +870,7 @@ mod tests { short.worker_state = Some(protocol::WorkerStateSnapshot { state: protocol::WorkerState::Idle, last_command_id: 0, + last_finished_submission_request_id: None, }); let mut long = worker("runtime-a", "worker-b", None); diff --git a/crates/tui/src/console/mod.rs b/crates/tui/src/console/mod.rs index cdaba24c1..559139b39 100644 --- a/crates/tui/src/console/mod.rs +++ b/crates/tui/src/console/mod.rs @@ -508,6 +508,7 @@ impl RetainedSnapshotSocket { }, state: protocol::WorkerStateSnapshot { last_command_id: 0, + last_finished_submission_request_id: None, state: protocol::WorkerState::Idle, }, in_flight: protocol::InFlightSnapshot::default(), diff --git a/crates/worker-runtime/src/catalog.rs b/crates/worker-runtime/src/catalog.rs index bf2bb4105..ed393fca9 100644 --- a/crates/worker-runtime/src/catalog.rs +++ b/crates/worker-runtime/src/catalog.rs @@ -367,6 +367,10 @@ pub struct WorkerDetail { pub created_at_ms: Option, /// Whether the persisted execution metadata was valid when this identity was loaded. pub execution_metadata_available: bool, + /// Whether this Runtime process reconstructed the Worker from durable execution state. + /// This proves that work from the prior process incarnation is no longer running. + #[serde(default)] + pub execution_reconstructed: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub worker_state: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/crates/worker-runtime/src/execution.rs b/crates/worker-runtime/src/execution.rs index 82881d2d5..a832d5350 100644 --- a/crates/worker-runtime/src/execution.rs +++ b/crates/worker-runtime/src/execution.rs @@ -514,6 +514,13 @@ pub struct WorkerSessionHistoryRequest { pub limit: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WorkerSessionAttachmentRequest { + pub worker_ref: WorkerRef, + pub session_id: String, + pub attachment_id: String, +} + pub trait WorkerExecutionBackend: Send + Sync + 'static { fn backend_id(&self) -> &str; @@ -544,6 +551,14 @@ pub trait WorkerExecutionBackend: Send + Sync + 'static { } } + fn worker_session_attachment( + &self, + _request: WorkerSessionAttachmentRequest, + ) -> Result + { + Err(session_store::RetainedAttachmentReadError::RetentionMissing) + } + fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult; /// Read-only restore validation performed before any live controller, @@ -780,6 +795,14 @@ impl WorkerExecutionBackendRef { self.backend.worker_session_history(request) } + pub(crate) fn worker_session_attachment( + &self, + request: WorkerSessionAttachmentRequest, + ) -> Result + { + self.backend.worker_session_attachment(request) + } + pub(crate) fn spawn_worker( &self, request: WorkerExecutionSpawnRequest, diff --git a/crates/worker-runtime/src/fs_store.rs b/crates/worker-runtime/src/fs_store.rs index 8869484b4..c2fb52bf7 100644 --- a/crates/worker-runtime/src/fs_store.rs +++ b/crates/worker-runtime/src/fs_store.rs @@ -523,6 +523,7 @@ pub(crate) struct PersistedWorkerRecord { pub(crate) config_bundle: Option, pub(crate) created_at_ms: Option, pub(crate) status: WorkerStatus, + pub(crate) last_finished_submission_request_id: Option, pub(crate) execution_state: PersistedWorkerExecutionState, pub(crate) workspace_id: Option, pub(crate) workdir_attachments: Vec, @@ -1824,6 +1825,8 @@ struct WorkerAggregateRecord { created_at_ms: Option, status: WorkerStatus, #[serde(default, skip_serializing_if = "Option::is_none")] + last_finished_submission_request_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] workspace_id: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] workdir_attachments: Vec, @@ -1874,6 +1877,7 @@ impl WorkerAggregateRecord { config_bundle: worker.config_bundle.clone(), created_at_ms: worker.created_at_ms, status: worker.status, + last_finished_submission_request_id: worker.last_finished_submission_request_id.clone(), workspace_id: worker.workspace_id.clone(), workdir_attachments: worker.workdir_attachments.clone(), logical_workdir_attachments: worker.logical_workdir_attachments.clone(), @@ -1893,6 +1897,7 @@ impl WorkerAggregateRecord { } fn validate(self, path: &Path) -> Result { + let last_finished_submission_request_id = self.last_finished_submission_request_id; let identity = WorkerIdentityRecord { schema_version: self.schema_version, worker_ref: self.worker_ref, @@ -1984,7 +1989,9 @@ impl WorkerAggregateRecord { } WorkerExecutionStateRecord::Unavailable => PersistedWorkerExecutionState::Unavailable, }; - Ok(identity.into_persisted(execution_state)) + let mut persisted = identity.into_persisted(execution_state); + persisted.last_finished_submission_request_id = last_finished_submission_request_id; + Ok(persisted) } } @@ -2164,6 +2171,7 @@ impl WorkerIdentityRecord { config_bundle: self.config_bundle, created_at_ms: self.created_at_ms, status: self.status, + last_finished_submission_request_id: None, execution_state, workspace_id: self.workspace_id, workdir_attachments, diff --git a/crates/worker-runtime/src/http_server.rs b/crates/worker-runtime/src/http_server.rs index 77b60defa..b4967073d 100644 --- a/crates/worker-runtime/src/http_server.rs +++ b/crates/worker-runtime/src/http_server.rs @@ -336,6 +336,13 @@ fn runtime_http_router_with_auth_and_ssh_keyscan_program( .route( remaining_route(runtime_api::RUNTIME_ROUTE_WORKER_ATTACHMENT, &["DELETE"]), delete(delete_worker_uploaded_file), + ) + .route( + remaining_route( + runtime_api::RUNTIME_ROUTE_WORKER_SESSION_ATTACHMENT, + &["GET"], + ), + get(get_worker_session_attachment), ); #[cfg(feature = "ws-server")] @@ -1980,6 +1987,60 @@ async fn delete_worker_uploaded_file( })) } +async fn get_worker_session_attachment( + State(state): State, + auth: Option>, + Path((worker_id, session_id, attachment_id)): Path<(String, String, String)>, + Query(request): Query, +) -> Result { + let worker_ref = worker_ref_for(&state.runtime, worker_id)?; + let scope = auth_workspace_scope(&state, auth.as_ref())?.ok_or_else(|| { + RuntimeHttpRestError::new( + StatusCode::FORBIDDEN, + "runtime_worker_session_scope_required", + "Worker Session attachment reads require a Workspace-scoped capability", + ) + })?; + if scope.workspace_id != request.workspace_id { + return Err(RuntimeHttpRestError::new( + StatusCode::FORBIDDEN, + "runtime_worker_session_workspace_scope_mismatch", + "Worker Session Workspace scope does not match the authenticated capability", + )); + } + let attachment = state + .runtime + .worker_session_attachment_scoped(&scope, &worker_ref, session_id, attachment_id) + .map_err(|error| { + use session_store::RetainedAttachmentReadError as Error; + let (status, code) = match error { + Error::RetentionMissing | Error::ActivePointerMissing => { + (StatusCode::GONE, "session_attachment_expired") + } + Error::SessionMismatch | Error::NotFound => { + (StatusCode::NOT_FOUND, "session_attachment_not_found") + } + Error::ResourceLimit => ( + StatusCode::PAYLOAD_TOO_LARGE, + "session_attachment_too_large", + ), + Error::MigrationRequired | Error::CorruptLog | Error::StorageUnavailable => ( + StatusCode::SERVICE_UNAVAILABLE, + "session_attachment_unavailable", + ), + }; + RuntimeHttpRestError::new(status, code, error.to_string()) + })?; + let content_type = axum::http::HeaderValue::from_str(&attachment.media_type).map_err(|_| { + RuntimeHttpRestError::new( + StatusCode::SERVICE_UNAVAILABLE, + "session_attachment_media_type_invalid", + "Session attachment media type is invalid", + ) + })?; + Ok(([(header::CONTENT_TYPE, content_type)], attachment.data).into_response()) +} + #[allow(dead_code)] async fn stop_worker( State(state): State, @@ -2289,7 +2350,7 @@ fn required_runtime_permission(method: &Method, path: &str) -> Option<&'static s } if path.ends_with("/input") || path.ends_with("/restore") - || path.contains("/attachments") + || (path.contains("/attachments") && *method != Method::GET) || path.contains("/workdir-attachments") { return Some("workers:input"); @@ -3031,6 +3092,13 @@ mod tests { ), Some("workers:input") ); + assert_eq!( + required_runtime_permission( + &Method::GET, + "/v1/workers/7/sessions/session-1/attachments/attachment-1" + ), + Some("workers:read") + ); assert_eq!( required_runtime_permission(&Method::POST, "/v1/workers/7/workdir-attachments"), Some("workers:input") diff --git a/crates/worker-runtime/src/runtime.rs b/crates/worker-runtime/src/runtime.rs index 01de33d4e..96622fb2f 100644 --- a/crates/worker-runtime/src/runtime.rs +++ b/crates/worker-runtime/src/runtime.rs @@ -1095,6 +1095,8 @@ impl Runtime { worker_id: worker_id.clone(), status: WorkerStatus::Stopped, worker_state: None, + last_finished_submission_request_id: None, + execution_reconstructed: false, workspace_id: scope.map(|scope| scope.workspace_id.clone()), profile: durable_request.profile.clone(), display_name: durable_request.display_name.clone(), @@ -1673,6 +1675,39 @@ impl Runtime { }) } + pub fn worker_session_attachment_scoped( + &self, + scope: &RuntimeWorkspaceScope, + worker_ref: &WorkerRef, + session_id: String, + attachment_id: String, + ) -> Result + { + let operation_lock = self + .worker_operation_lock(worker_ref.worker_id) + .map_err(|_| session_store::RetainedAttachmentReadError::RetentionMissing)?; + let _operation_guard = operation_lock + .lock() + .map_err(|_| session_store::RetainedAttachmentReadError::StorageUnavailable)?; + self.ensure_worker_in_workspace(scope, worker_ref) + .map_err(|_| session_store::RetainedAttachmentReadError::NotFound)?; + let backend = { + let state = self + .lock() + .map_err(|_| session_store::RetainedAttachmentReadError::StorageUnavailable)?; + state + .worker(worker_ref) + .map_err(|_| session_store::RetainedAttachmentReadError::NotFound)?; + state.execution_backend.clone() + } + .ok_or(session_store::RetainedAttachmentReadError::StorageUnavailable)?; + backend.worker_session_attachment(crate::execution::WorkerSessionAttachmentRequest { + worker_ref: worker_ref.clone(), + session_id, + attachment_id, + }) + } + /// Attach a live execution through a workspace-scoped Runtime authorization context. pub fn restore_worker_scoped( &self, @@ -3386,6 +3421,16 @@ impl Runtime { candidate.pending_stop = None; candidate.restore_candidate_context = None; candidate.status = status; + let mut worker_state = worker_state; + if worker_state.last_finished_submission_request_id.is_none() { + // The restored controller starts with fresh live state, but this + // historical fence proves that a previously accepted Submit Run + // already returned. Preserve it across process reconstruction so + // durable consumers can finish post-restart cleanup without + // waiting for an old request to run again. + worker_state.last_finished_submission_request_id = + candidate.last_finished_submission_request_id.clone(); + } let _ = candidate.apply_worker_state(&worker_state); candidate.restore_intent = restore_intent_for_status(candidate.status); candidate.workdir_attachments = workdir_attachments; @@ -3996,6 +4041,8 @@ impl RuntimeState { worker_id: worker.worker_id, status: worker.status, worker_state: None, + last_finished_submission_request_id: worker.last_finished_submission_request_id, + execution_reconstructed: true, workspace_id: worker.workspace_id, profile: worker.profile, display_name: worker.display_name, @@ -4893,6 +4940,8 @@ struct WorkerRecord { worker_id: WorkerId, status: WorkerStatus, worker_state: Option, + last_finished_submission_request_id: Option, + execution_reconstructed: bool, workspace_id: Option, profile: ProfileSelector, display_name: Option, @@ -4914,6 +4963,9 @@ struct WorkerRecord { impl WorkerRecord { fn apply_worker_state(&mut self, incoming: &protocol::WorkerStateSnapshot) { + if let Some(request_id) = &incoming.last_finished_submission_request_id { + self.last_finished_submission_request_id = Some(request_id.clone()); + } self.worker_state = Some(incoming.clone()); } @@ -4959,6 +5011,7 @@ impl WorkerRecord { status: self.status, created_at_ms: self.created_at_ms, execution_metadata_available: self.execution_metadata_available, + execution_reconstructed: self.execution_reconstructed, worker_state: self.worker_state.clone(), workspace_id: self.workspace_id.clone(), workdir_attachments: self.workdir_attachments.clone(), @@ -5022,6 +5075,7 @@ impl WorkerRecord { config_bundle: self.config_bundle.clone(), created_at_ms: self.created_at_ms, status: self.status, + last_finished_submission_request_id: self.last_finished_submission_request_id.clone(), execution_state, workspace_id: self.workspace_id.clone(), workdir_attachments: self.workdir_attachments.clone(), @@ -8028,6 +8082,7 @@ mod tests { protocol::WorkerRunState::Running, )), last_command_id: 2, + last_finished_submission_request_id: Some("request-old".to_string()), }; { let mut state = runtime.lock().unwrap(); @@ -8049,6 +8104,7 @@ mod tests { let fresh = protocol::WorkerStateSnapshot { state: protocol::WorkerState::Idle, last_command_id: 0, + last_finished_submission_request_id: None, }; { let mut state = runtime.lock().unwrap(); @@ -9196,6 +9252,81 @@ mod tests { let _ = std::fs::remove_dir_all(root); } + #[cfg(feature = "fs-store")] + #[test] + fn fs_store_preserves_finished_submission_fence_across_active_restore() { + let root = fs_store_root("finished-submission-restore"); + let options = crate::fs_store::FsRuntimeStoreOptions { + root: root.clone(), + runtime_id: "test-runtime".to_string(), + display_name: None, + }; + let execution = Arc::new(TestExecutionBackend::default()); + let runtime = + Runtime::with_fs_store_and_execution_backend(options.clone(), execution.clone()) + .unwrap(); + runtime.store_config_bundle(test_bundle()).unwrap(); + let worker = runtime + .create_worker(task_request("persist finished submission fence")) + .unwrap(); + execution.set_dispatch_result( + WorkerExecutionResult::accepted_submission( + WorkerExecutionOperation::Input, + "job-attempt-request", + "submission-id", + protocol::SubmissionDisposition::Started, + ) + .with_worker_state(protocol::WorkerStateSnapshot { + state: protocol::WorkerState::Idle, + // Production Submit Run does not consume a lifecycle command + // id, so this intentionally remains zero. + last_command_id: 0, + last_finished_submission_request_id: Some("job-attempt-request".to_string()), + }), + ); + let mut input = WorkerInput::user("run durable Backend Job attempt"); + input.submission_request_id = Some("job-attempt-request".to_string()); + runtime.send_input(&worker.worker_ref, input).unwrap(); + assert!( + !runtime + .worker_detail(&worker.worker_ref) + .unwrap() + .execution_reconstructed + ); + + let aggregate_path = root + .join("workers") + .join(worker.worker_id.to_string()) + .join("worker.json"); + let aggregate: serde_json::Value = + serde_json::from_slice(&std::fs::read(&aggregate_path).unwrap()).unwrap(); + assert_eq!( + aggregate["last_finished_submission_request_id"], + serde_json::json!("job-attempt-request") + ); + drop(runtime); + + let restored = Runtime::with_fs_store_and_execution_backend( + options, + Arc::new(TestExecutionBackend::default()), + ) + .unwrap(); + let restored_detail = restored.worker_detail(&worker.worker_ref).unwrap(); + assert!(restored_detail.execution_reconstructed); + let restored_state = restored_detail + .worker_state + .expect("automatic restore publishes fresh Worker state"); + assert_eq!(restored_state.last_command_id, 0); + assert_eq!( + restored_state + .last_finished_submission_request_id + .as_deref(), + Some("job-attempt-request") + ); + + let _ = std::fs::remove_dir_all(root); + } + #[cfg(feature = "fs-store")] #[test] fn fs_store_explicitly_retries_rejected_automatic_restore() { diff --git a/crates/worker-runtime/src/worker_backend.rs b/crates/worker-runtime/src/worker_backend.rs index 1fabdf3a4..690e52623 100644 --- a/crates/worker-runtime/src/worker_backend.rs +++ b/crates/worker-runtime/src/worker_backend.rs @@ -186,6 +186,16 @@ pub trait RuntimeWorkerFactory: Send + Sync + 'static { Err(session_store::RetainedHistoryReadError::RetentionMissing) } + fn retained_session_attachment( + &self, + _worker_ref: &WorkerRef, + _session_id: &str, + _attachment_id: &str, + ) -> Result + { + Err(session_store::RetainedAttachmentReadError::RetentionMissing) + } + async fn spawn_controller( &self, request: WorkerExecutionSpawnRequest, @@ -1116,6 +1126,27 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory { ) } + fn retained_session_attachment( + &self, + worker_ref: &WorkerRef, + session_id: &str, + attachment_id: &str, + ) -> Result + { + let aggregate_dir = self + .worker_aggregate_dir(worker_ref) + .map_err(|_| session_store::RetainedAttachmentReadError::RetentionMissing)?; + let worker_name = Self::runtime_worker_name_for_ref(worker_ref); + session_store::read_retained_session_attachment( + &aggregate_dir, + &worker_name, + session_id, + attachment_id, + session_store::DEFAULT_RETAINED_HISTORY_MAX_SCAN_BYTES, + 10 * 1024 * 1024, + ) + } + fn observe_workspace_prompt_projection( &self, projection: worker::WorkspacePromptProjection, @@ -2412,6 +2443,36 @@ where } } + fn worker_session_attachment( + &self, + request: crate::execution::WorkerSessionAttachmentRequest, + ) -> Result + { + let live = self + .workers + .lock() + .map_err(|_| session_store::RetainedAttachmentReadError::StorageUnavailable)? + .get(&request.worker_ref) + .map(|execution| execution.handle.clone()); + if let Some(handle) = live { + match handle.session_attachment( + &request.session_id, + &request.attachment_id, + 10 * 1024 * 1024, + ) { + Ok(attachment) => return Ok(attachment), + Err(session_store::RetainedAttachmentReadError::SessionMismatch) + | Err(session_store::RetainedAttachmentReadError::NotFound) => {} + Err(error) => return Err(error), + } + } + self.factory.retained_session_attachment( + &request.worker_ref, + &request.session_id, + &request.attachment_id, + ) + } + fn observe_workspace_prompt_projection( &self, projection: worker::WorkspacePromptProjection, @@ -3824,11 +3885,13 @@ mod tests { protocol::WorkerRunState::Running, )), last_command_id: 2, + last_finished_submission_request_id: Some("request-old".to_string()), }; let current = Arc::new(RwLock::new(running)); let idle = protocol::WorkerStateSnapshot { state: protocol::WorkerState::Idle, last_command_id: 0, + last_finished_submission_request_id: None, }; let mut replacement = Event::WorkerState { snapshot: idle.clone(), @@ -3841,6 +3904,7 @@ mod tests { protocol::WorkerRunState::Paused, )), last_command_id: 3, + last_finished_submission_request_id: None, }; let mut acknowledgement = Event::CommandAcknowledged { acknowledgement: protocol::WorkerCommandAcknowledgement { diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index cb8cba96d..1b83ff393 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -129,6 +129,27 @@ impl WorkerHandle { self.sink.subscribe_with_snapshot().0 } + pub fn session_attachment( + &self, + session_id: &str, + attachment_id: &str, + max_attachment_bytes: u64, + ) -> Result + { + if session_id == self.session_id.to_string() { + return session_store::read_session_attachment_from_entries( + &self.committed_entries(), + attachment_id, + max_attachment_bytes, + ); + } + self.spawned_registry + .session_attachment(session_id, attachment_id, max_attachment_bytes) + .unwrap_or(Err( + session_store::RetainedAttachmentReadError::SessionMismatch, + )) + } + pub fn snapshot_event(&self) -> Event { self.snapshot_event_with_entry_subscription().0 } @@ -300,6 +321,7 @@ async fn finish_controller_run( runtime_dir: &RuntimeDir, working_event_tx: &broadcast::Sender, new_status: WorkerStatus, + finished_submission_request_id: Option, ) where C: LlmClient + Clone + 'static, St: Store + WorkerMetadataStore + Clone + 'static, @@ -320,7 +342,20 @@ async fn finish_controller_run( Some(worker.total_tokens().tokens), protocol::ContextTokenSource::Estimated, ); - set_controller_status(shared_state, runtime_dir, working_event_tx, new_status).await; + if let Some(submission_request_id) = finished_submission_request_id { + let state = match new_status { + WorkerStatus::Idle | WorkerStatus::Stopped => WorkerState::Idle, + WorkerStatus::Running => { + WorkerState::Busy(WorkerBusyState::Run(WorkerRunState::Running)) + } + WorkerStatus::Paused => WorkerState::Busy(WorkerBusyState::Run(WorkerRunState::Paused)), + }; + let snapshot = shared_state.finish_submission_run(state, submission_request_id); + let _ = runtime_dir.write_status(shared_state).await; + let _ = working_event_tx.send(Event::WorkerState { snapshot }); + } else { + set_controller_status(shared_state, runtime_dir, working_event_tx, new_status).await; + } } /// Pending turn launch staged by an event handler for the next outer-loop @@ -2023,6 +2058,10 @@ async fn controller_loop( // interrupted/error turn from being carried into the next snapshot. worker.clear_in_flight_events(); let parent_originated = run.is_parent_originated(); + let finished_submission_request_id = match &run { + PendingRun::Submit(submission) => Some(submission.submission_request_id.clone()), + PendingRun::RunForNotification { .. } | PendingRun::Resume => None, + }; let user_input_submit = matches!(&run, PendingRun::Submit(_)); if !user_input_submit { set_controller_status( @@ -2121,12 +2160,16 @@ async fn controller_loop( } } } + let finished_submission_request_id = (new_status != WorkerStatus::Paused) + .then_some(finished_submission_request_id) + .flatten(); finish_controller_run( &mut worker, &shared_state, &runtime_dir, &working_event_tx, new_status, + finished_submission_request_id, ) .await; if shutdown { diff --git a/crates/worker/src/feature/builtin/manage_worker.rs b/crates/worker/src/feature/builtin/manage_worker.rs index 4155db86d..5c746f9b6 100644 --- a/crates/worker/src/feature/builtin/manage_worker.rs +++ b/crates/worker/src/feature/builtin/manage_worker.rs @@ -522,6 +522,7 @@ fn workspace_worker_create_request( relative_cwd: attachment.relative_cwd, }) .collect(), + feature_connections: Default::default(), control_operation_id: Some(control_operation_id), }) } diff --git a/crates/worker/src/internal_worker.rs b/crates/worker/src/internal_worker.rs index 47642a2e1..ce6bff263 100644 --- a/crates/worker/src/internal_worker.rs +++ b/crates/worker/src/internal_worker.rs @@ -335,6 +335,7 @@ fn send_internal_worker_state( let _ = event_tx.send(Event::WorkerState { snapshot: protocol::WorkerStateSnapshot { last_command_id: 0, + last_finished_submission_request_id: None, state, }, }); @@ -521,6 +522,19 @@ impl InternalWorkerSessionHandle { .unwrap_or_default() } + pub(crate) fn session_attachment( + &self, + attachment_id: &str, + max_attachment_bytes: u64, + ) -> Result + { + session_store::read_session_attachment_from_entries( + &self.entries(), + attachment_id, + max_attachment_bytes, + ) + } + pub(crate) async fn send( &self, input: impl Into, @@ -1447,6 +1461,50 @@ permission = "write" assert_eq!(result.identity.kind, "test"); } + #[tokio::test] + async fn visible_internal_session_resolves_its_projected_image_attachment() { + let (handle, _events) = + test_internal_worker_session(InternalWorkerVisibility::ParentClient); + let image = vec![1, 3, 5, 7]; + let entry_id = "internal-image-entry"; + let attachment_id = + session_store::session_tool_attachment_id(entry_id, 0, "image/png", &image); + handle.publish_test_entry(LogEntry::AnnotatedToolResult { + ts: 1, + entry: session_store::LoggedHistoryEntry { + item: session_store::LoggedItem::ToolResult { + call_id: "internal-image-call".into(), + summary: "internal screenshot".into(), + content: None, + attachments: vec![session_store::logged_item::LoggedAttachment::Image { + mime_type: "image/png".into(), + data: image.clone(), + }], + disposition: agen::tool::ToolResultDisposition::Success, + is_error: false, + }, + metadata: session_store::LoggedSessionHistoryMetadata { + entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()), + origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, + derivation: None, + }, + }, + }); + + let projected = handle.protocol_snapshot(); + let protocol::SessionSnapshotEntryData::ToolResult { attachments, .. } = + &projected.session.entries[0].data + else { + panic!("expected projected tool result"); + }; + assert_eq!(attachments[0].attachment_id, attachment_id); + let attachment = handle + .session_attachment(&attachment_id, 1024) + .expect("internal image body"); + assert_eq!(attachment.media_type, "image/png"); + assert_eq!(attachment.data, image); + } + #[test] fn internal_turn_result_mapping_is_exhaustive() { let cases = [ diff --git a/crates/worker/src/shared_state.rs b/crates/worker/src/shared_state.rs index 8da06e535..46d0690d0 100644 --- a/crates/worker/src/shared_state.rs +++ b/crates/worker/src/shared_state.rs @@ -117,6 +117,23 @@ impl WorkerSharedState { snapshot.clone() } + /// Publish the post-Run state and the exact Submit request whose controller + /// invocation has returned. Updating both in one snapshot prevents observers + /// from treating an earlier Idle state as evidence for a later Run. + pub(crate) fn finish_submission_run( + &self, + state: WorkerState, + submission_request_id: String, + ) -> WorkerStateSnapshot { + let mut snapshot = self + .state + .write() + .expect("worker state lock poisoned; refusing an inferred fallback state"); + snapshot.state = state; + snapshot.last_finished_submission_request_id = Some(submission_request_id); + snapshot.clone() + } + pub(crate) fn admit_command( &self, envelope: WorkerCommandEnvelope, @@ -305,6 +322,14 @@ mod tests { assert_eq!(snapshot.state, paused); assert_eq!(snapshot.last_command_id, 0); assert_eq!(state.catalog_status(), WorkerStatus::Paused); + + let finished = state.finish_submission_run(WorkerState::Idle, "request-1".to_string()); + assert_eq!(finished.state, WorkerState::Idle); + assert_eq!( + finished.last_finished_submission_request_id.as_deref(), + Some("request-1") + ); + assert_eq!(finished.last_command_id, 0); } #[test] @@ -319,6 +344,7 @@ mod tests { state.snapshot(), WorkerStateSnapshot { last_command_id: 9, + last_finished_submission_request_id: None, state: WorkerState::Idle, } ); diff --git a/crates/worker/src/spawn/registry.rs b/crates/worker/src/spawn/registry.rs index 5b706f792..037cba6ee 100644 --- a/crates/worker/src/spawn/registry.rs +++ b/crates/worker/src/spawn/registry.rs @@ -774,6 +774,41 @@ impl SpawnedWorkerRegistry { }); } + pub(crate) fn session_attachment( + &self, + session_id: &str, + attachment_id: &str, + max_attachment_bytes: u64, + ) -> Option< + Result< + session_store::RetainedSessionAttachment, + session_store::RetainedAttachmentReadError, + >, + > { + let internal_session = self + .internal_records + .lock() + .ok()? + .iter() + .find(|record| { + record.session.visibility() == InternalWorkerVisibility::ParentClient + && record.session.session_id_string() == session_id + }) + .map(|record| record.session.clone()); + let session = internal_session.or_else(|| { + self.service_records + .lock() + .ok()? + .iter() + .find(|record| { + record.session.visibility() == InternalWorkerVisibility::ParentClient + && record.session.session_id_string() == session_id + }) + .map(|record| record.session.clone()) + })?; + Some(session.session_attachment(attachment_id, max_attachment_bytes)) + } + pub(crate) fn internal_worker_snapshots(&self) -> Vec { let parent_session_id = self .parent_protocol @@ -1258,6 +1293,59 @@ mod tests { ) } + #[tokio::test] + async fn parent_visible_internal_attachment_resolves_by_session_identity() { + let registry = registry(); + let (visible, _visible_tx) = + record("visible", InternalWorkerVisibility::ParentClient).await; + let session_id = visible.session.session_id_string(); + let image = vec![2, 4, 6, 8]; + let entry_id = "visible-image-entry"; + let attachment_id = + session_store::session_tool_attachment_id(entry_id, 0, "image/png", &image); + visible + .session + .publish_test_entry(LogEntry::AnnotatedToolResult { + ts: 1, + entry: session_store::LoggedHistoryEntry { + item: session_store::LoggedItem::ToolResult { + call_id: "visible-image-call".into(), + summary: "visible image".into(), + content: None, + attachments: vec![session_store::logged_item::LoggedAttachment::Image { + mime_type: "image/png".into(), + data: image.clone(), + }], + disposition: agen::tool::ToolResultDisposition::Success, + is_error: false, + }, + metadata: session_store::LoggedSessionHistoryMetadata { + entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()), + origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, + derivation: None, + }, + }, + }); + registry.internal_records.lock().unwrap().push(visible); + + let attachment = registry + .session_attachment(&session_id, &attachment_id, 1024) + .expect("visible session") + .expect("visible attachment"); + assert_eq!(attachment.data, image); + + let (private, _private_tx) = + record("private", InternalWorkerVisibility::ServicePrivate).await; + let private_session_id = private.session.session_id_string(); + registry.internal_records.lock().unwrap().push(private); + assert!( + registry + .session_attachment(&private_session_id, &attachment_id, 1024) + .is_none(), + "service-private sessions must not become parent-fetchable" + ); + } + #[tokio::test] async fn visible_internal_worker_starts_with_own_live_snapshot_metadata() { let registry = registry(); diff --git a/crates/worker/tests/controller_test.rs b/crates/worker/tests/controller_test.rs index baf542552..c1b4668e4 100644 --- a/crates/worker/tests/controller_test.rs +++ b/crates/worker/tests/controller_test.rs @@ -1299,12 +1299,10 @@ async fn run_end_returns_to_idle_without_busy_status() { let worker = make_worker(client).await; let handle = spawn_controller(worker).await; let mut rx = handle.subscribe(); + let submission_request_id = protocol::new_submission_request_id(); handle - .send(Method::submit_text( - protocol::new_submission_request_id(), - "Hello", - )) + .send(Method::submit_text(submission_request_id.clone(), "Hello")) .await .unwrap(); @@ -1319,7 +1317,11 @@ async fn run_end_returns_to_idle_without_busy_status() { saw_run_end = true; } Ok(Event::WorkerState { snapshot }) - if saw_run_end && snapshot.catalog_status() == WorkerStatus::Idle => { + if saw_run_end + && snapshot.catalog_status() == WorkerStatus::Idle + && snapshot.last_finished_submission_request_id.as_deref() + == Some(submission_request_id.as_str()) => + { saw_idle_status = true; break; } @@ -1334,7 +1336,7 @@ async fn run_end_returns_to_idle_without_busy_status() { assert!(saw_run_end, "expected RunEnd::Finished"); assert!( saw_idle_status, - "expected idle status immediately after RunEnd" + "expected exact Submit teardown fence in idle status immediately after RunEnd" ); assert_eq!(handle.shared_state.catalog_status(), WorkerStatus::Idle); } diff --git a/crates/workspace-server/Cargo.toml b/crates/workspace-server/Cargo.toml index 94d5c0bb7..521b7f4ac 100644 --- a/crates/workspace-server/Cargo.toml +++ b/crates/workspace-server/Cargo.toml @@ -40,6 +40,7 @@ tokio-tungstenite = { workspace = true, features = ["rustls-tls-webpki-roots"] } worker.workspace = true workdir = { workspace = true, features = ["http-client"] } worker-runtime.workspace = true +session-store.workspace = true toml.workspace = true tracing.workspace = true tracing-subscriber.workspace = true @@ -51,7 +52,6 @@ webauthn-rs = { workspace = true } [dev-dependencies] agen.workspace = true -session-store.workspace = true tempfile.workspace = true tools.workspace = true yoi = { path = "../yoi" } diff --git a/crates/workspace-server/src/backend_job.rs b/crates/workspace-server/src/backend_job.rs index e7f7bce77..08effd749 100644 --- a/crates/workspace-server/src/backend_job.rs +++ b/crates/workspace-server/src/backend_job.rs @@ -284,6 +284,38 @@ pub struct BackendJobRecord { pub completed_at: Option, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BackendJobWorkerCleanupState { + Pending, + Executing, + Completed, + Failed, +} + +impl BackendJobWorkerCleanupState { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Pending => "pending", + Self::Executing => "executing", + Self::Completed => "completed", + Self::Failed => "failed", + } + } + + pub(crate) fn parse(value: &str) -> Result { + match value { + "pending" => Ok(Self::Pending), + "executing" => Ok(Self::Executing), + "completed" => Ok(Self::Completed), + "failed" => Ok(Self::Failed), + other => Err(Error::Store(format!( + "unknown Backend Job Worker cleanup state `{other}`" + ))), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct BackendJobAttemptRecord { pub workspace_id: String, @@ -300,6 +332,11 @@ pub struct BackendJobAttemptRecord { pub result_digest: Option, pub failure_category: Option, pub failure_detail: Option, + pub worker_cleanup_state: Option, + pub worker_cleanup_failure_category: Option, + pub worker_cleanup_failure_detail: Option, + pub worker_cleanup_updated_at: Option, + pub worker_cleanup_completed_at: Option, pub created_at: String, pub updated_at: String, pub completed_at: Option, diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index 14b4381ec..b9f3ed39a 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -259,6 +259,9 @@ pub struct InternalWorkerSummary { pub availability: protocol::subscription::SubscriptionWorkerAvailability, /// Runtime catalog lifecycle compatibility state. pub state: String, + /// Runtime-process reconstruction evidence used only for lifecycle coordination. + #[serde(skip)] + pub execution_reconstructed: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub worker_state: Option, pub last_seen_at: Option, @@ -770,6 +773,12 @@ pub struct WorkerProxyConnectPoint { pub diagnostics: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeSessionAttachment { + pub media_type: String, + pub data: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum RuntimeRegistryError { InvalidIdentifier { @@ -1220,6 +1229,23 @@ pub trait WorkspaceWorkerRuntime: Send + Sync { }) } + fn worker_session_attachment( + &self, + _workspace_id: &str, + worker_ref: &EmbeddedWorkerRef, + _session_id: &str, + _attachment_id: &str, + ) -> Result { + Err(RuntimeRegistryError::RuntimeOperationFailed { + runtime_id: self.runtime_id().to_string(), + code: "session_attachment_unsupported".to_string(), + message: format!( + "Session attachment reads are not supported for Worker `{}`", + worker_ref.worker_id + ), + }) + } + fn session_public_search( &self, _request: runtime_api::SessionPublicSearchRequest, @@ -2096,6 +2122,27 @@ impl RuntimeRegistry { }) } + pub fn worker_session_attachment( + &self, + workspace_id: &str, + worker: &RuntimeWorkerRef, + session_id: &str, + attachment_id: &str, + ) -> Result { + validate_backend_identifier("runtime_id", &worker.runtime_id)?; + validate_backend_identifier("worker_id", &worker.worker_id)?; + validate_backend_identifier("session_id", session_id)?; + validate_backend_identifier("attachment_id", attachment_id)?; + let runtime = self.runtime(&worker.runtime_id)?; + let worker_ref = + EmbeddedWorkerRef::new(EmbeddedWorkerId::parse(&worker.worker_id).ok_or_else( + || RuntimeRegistryError::UnknownWorker { + worker: worker.clone(), + }, + )?); + runtime.worker_session_attachment(workspace_id, &worker_ref, session_id, attachment_id) + } + pub fn session_public_search( &self, runtime_id: &str, @@ -2333,6 +2380,7 @@ impl EmbeddedWorkerRuntime { summary.execution_metadata_available, ) .to_string(), + execution_reconstructed: false, worker_state: summary.worker_state.clone(), last_seen_at: None, pinned: false, @@ -2373,6 +2421,7 @@ impl EmbeddedWorkerRuntime { availability: protocol::subscription::SubscriptionWorkerAvailability::Observed, state: embedded_worker_state_label(detail.status, detail.execution_metadata_available) .to_string(), + execution_reconstructed: detail.execution_reconstructed, worker_state: detail.worker_state.clone(), last_seen_at: None, pinned: false, @@ -3017,6 +3066,46 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime { .map_err(|error| error.to_string()) } + fn worker_session_attachment( + &self, + workspace_id: &str, + worker_ref: &EmbeddedWorkerRef, + session_id: &str, + attachment_id: &str, + ) -> Result { + let scope = RuntimeWorkspaceScope::new(workspace_id, "embedded-backend"); + self.runtime + .worker_session_attachment_scoped( + &scope, + worker_ref, + session_id.to_string(), + attachment_id.to_string(), + ) + .map(|attachment| RuntimeSessionAttachment { + media_type: attachment.media_type, + data: attachment.data, + }) + .map_err(|error| RuntimeRegistryError::RuntimeOperationFailed { + runtime_id: self.runtime_id.clone(), + code: match error { + session_store::RetainedAttachmentReadError::RetentionMissing + | session_store::RetainedAttachmentReadError::ActivePointerMissing => { + "session_attachment_expired" + } + session_store::RetainedAttachmentReadError::SessionMismatch + | session_store::RetainedAttachmentReadError::NotFound => { + "session_attachment_not_found" + } + session_store::RetainedAttachmentReadError::ResourceLimit => { + "session_attachment_too_large" + } + _ => "session_attachment_unavailable", + } + .to_string(), + message: error.to_string(), + }) + } + fn session_public_search( &self, request: runtime_api::SessionPublicSearchRequest, @@ -3732,7 +3821,7 @@ fn workspace_runtime_operation(method: &str, path_and_query: &str) -> &'static s } if path.ends_with("/input") || path.ends_with("/restore") - || path.contains("/attachments") + || (path.contains("/attachments") && method != "GET") || path.contains("/workdir-attachments") { return "workers:input"; @@ -4059,6 +4148,64 @@ impl RemoteWorkerRuntime { self.send_json(path, "GET", &[], self.http.get(self.endpoint(path))) } + fn get_session_attachment( + &self, + path: &str, + ) -> Result { + let runtime_id = self.runtime_id.clone(); + let workspace_id = self.workspace_id.clone(); + let bearer_token = self.bearer_token.clone(); + let capability_token = match &self.workspace_authorization { + Some(authorization) => Some(authorization.issue( + "GET", + path, + workspace_runtime_operation("GET", path), + worker_id_from_remote_path(path).as_deref(), + &[], + )?), + None => None, + }; + let request = self.http.get(self.endpoint(path)); + run_blocking_http(move || { + let mut request = request.header(RUNTIME_WORKSPACE_SCOPE_HEADER, &workspace_id); + if let Some(token) = capability_token.as_deref().or(bearer_token.as_deref()) { + request = request.header(AUTHORIZATION, format!("Bearer {token}")); + } + let response = request + .send() + .map_err(|error| remote_reqwest_diagnostic(&runtime_id, error))?; + let status = response.status(); + if !status.is_success() { + return Err(remote_http_status_diagnostic(&runtime_id, status, response)); + } + let media_type = response + .headers() + .get(CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + let mut data = Vec::new(); + response + .take((MAX_REMOTE_RUNTIME_RESPONSE_BYTES + 1) as u64) + .read_to_end(&mut data) + .map_err(|_| { + diagnostic( + "remote_runtime_response_read_failed", + HostDiagnosticSeverity::Error, + "Remote Runtime attachment response could not be read".to_string(), + ) + })?; + if data.len() > MAX_REMOTE_RUNTIME_RESPONSE_BYTES { + return Err(diagnostic( + "remote_runtime_response_too_large", + HostDiagnosticSeverity::Error, + "Remote Runtime attachment response exceeded the allowed size".to_string(), + )); + } + Ok(RuntimeSessionAttachment { media_type, data }) + }) + } + fn post_json(&self, path: &str, body: &B) -> Result where B: Serialize + ?Sized, @@ -4226,6 +4373,7 @@ impl RemoteWorkerRuntime { summary.execution_metadata_available, ) .to_string(), + execution_reconstructed: false, worker_state: summary.worker_state.clone(), last_seen_at: None, pinned: false, @@ -4264,6 +4412,7 @@ impl RemoteWorkerRuntime { availability: protocol::subscription::SubscriptionWorkerAvailability::Observed, state: embedded_worker_state_label(detail.status, detail.execution_metadata_available) .to_string(), + execution_reconstructed: detail.execution_reconstructed, worker_state: detail.worker_state.clone(), last_seen_at: None, pinned: false, @@ -5093,6 +5242,29 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime { .map_err(|diagnostic| diagnostic.message) } + fn worker_session_attachment( + &self, + workspace_id: &str, + worker_ref: &EmbeddedWorkerRef, + session_id: &str, + attachment_id: &str, + ) -> Result { + let path = format!( + "/v1/workers/{}/sessions/{}/attachments/{}?workspace_id={}", + url_path_segment_encode(&worker_ref.worker_id.to_string()), + url_path_segment_encode(session_id), + url_path_segment_encode(attachment_id), + url_query_value_encode(workspace_id), + ); + self.get_session_attachment(&path).map_err(|diagnostic| { + RuntimeRegistryError::RuntimeOperationFailed { + runtime_id: self.runtime_id.clone(), + code: diagnostic.code, + message: diagnostic.message, + } + }) + } + fn session_public_search( &self, request: runtime_api::SessionPublicSearchRequest, @@ -6106,6 +6278,7 @@ pub fn placeholder_worker(host_id: impl Into) -> InternalWorkerSummary { }, availability: protocol::subscription::SubscriptionWorkerAvailability::Unavailable, state: "unsupported".to_string(), + execution_reconstructed: false, worker_state: None, last_seen_at: None, pinned: false, @@ -6834,6 +7007,7 @@ mod tests { }, availability: protocol::subscription::SubscriptionWorkerAvailability::Observed, state: "available".to_string(), + execution_reconstructed: false, worker_state: None, last_seen_at: None, pinned: false, @@ -7648,6 +7822,13 @@ mod tests { ), "workers:input" ); + assert_eq!( + workspace_runtime_operation( + "GET", + &format!("/v1/workers/{worker_id}/sessions/session-1/attachments/attachment-1") + ), + "workers:read" + ); assert_eq!( workspace_runtime_operation("POST", SSH_HOST_KEY_PROBE_PATH), SSH_HOST_KEY_PROBE_OPERATION diff --git a/crates/workspace-server/src/latest_schema.sql b/crates/workspace-server/src/latest_schema.sql index 3a24911fc..16d98f1f4 100644 --- a/crates/workspace-server/src/latest_schema.sql +++ b/crates/workspace-server/src/latest_schema.sql @@ -875,6 +875,13 @@ CREATE TABLE backend_job_attempts ( result_digest TEXT, failure_category TEXT, failure_detail TEXT, + worker_cleanup_state TEXT CHECK ( + worker_cleanup_state IN ('pending', 'executing', 'completed', 'failed') + ), + worker_cleanup_failure_category TEXT, + worker_cleanup_failure_detail TEXT, + worker_cleanup_updated_at TEXT, + worker_cleanup_completed_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, completed_at TEXT, @@ -889,6 +896,8 @@ CREATE TABLE backend_job_attempts ( ); CREATE INDEX backend_job_attempts_recovery ON backend_job_attempts(workspace_id, state, deadline_at); +CREATE INDEX backend_job_attempts_worker_cleanup + ON backend_job_attempts(workspace_id, worker_cleanup_state, updated_at); CREATE TABLE backend_job_deliveries ( workspace_id TEXT NOT NULL, delivery_id TEXT NOT NULL, diff --git a/crates/workspace-server/src/profile_settings.rs b/crates/workspace-server/src/profile_settings.rs index 0cc848b80..0e350cc42 100644 --- a/crates/workspace-server/src/profile_settings.rs +++ b/crates/workspace-server/src/profile_settings.rs @@ -447,6 +447,23 @@ pub fn build_virtual_profile_config_bundle_with_prompt_projection( Ok(Some(bundle)) } +pub(crate) fn resolve_profile_manifest_from_config_bundle( + bundle: &ConfigBundle, + selector: &str, +) -> Result { + let archive = bundle.profile_source_archive.as_ref().ok_or_else(|| { + Error::Config(format!( + "resolved Profile {selector:?} is missing its source archive" + )) + })?; + let verified = archive.verify().map_err(|error| { + Error::Config(format!("failed to verify Profile {selector:?}: {error}")) + })?; + verified + .resolve_profile(selector, Path::new("/"), "worker-launch-validation") + .map_err(|error| Error::Config(format!("failed to resolve Profile {selector:?}: {error}"))) +} + fn build_virtual_profile_archive( selector: &str, entry: &VirtualProfileEntry, diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index fef6c9b7c..414cf5981 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -7,9 +7,9 @@ use std::sync::{Arc, Mutex, RwLock, Weak}; use async_trait::async_trait; use axum::extract::ws::{CloseFrame, Message as WsMessage, WebSocket, WebSocketUpgrade}; use axum::extract::{DefaultBodyLimit, Extension, Path as AxumPath, Query, Request, State}; +use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE, LOCATION, ORIGIN}; #[cfg(test)] -use axum::http::header::{CACHE_CONTROL, ETAG, IF_NONE_MATCH}; -use axum::http::header::{CONTENT_TYPE, LOCATION, ORIGIN}; +use axum::http::header::{ETAG, IF_NONE_MATCH}; use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri}; use axum::middleware::{self, Next}; use axum::response::{IntoResponse, Response}; @@ -144,9 +144,10 @@ use crate::authority::{ SqliteWorkspaceAuthority, TicketAuthority, TicketMergeRevisionSource, merge_request_summary, }; use crate::backend_job::{ - ABSOLUTE_MAX_CONCURRENT_JOBS, BackendJobAttemptState, BackendJobDeliveryState, - BackendJobRequest, BackendJobReservation, BackendJobResultAcceptance, - BackendJobResultSubmission, BackendJobState, MAX_JOB_DELIVERY_BYTES, allocation_key, + ABSOLUTE_MAX_CONCURRENT_JOBS, BackendJobAttemptRecord, BackendJobAttemptState, + BackendJobDeliveryState, BackendJobRequest, BackendJobReservation, BackendJobResultAcceptance, + BackendJobResultSubmission, BackendJobState, BackendJobWorkerCleanupState, + MAX_JOB_DELIVERY_BYTES, allocation_key, }; use crate::companion::{ CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse, @@ -3417,14 +3418,12 @@ impl WorkspaceApi { Ok(spawned) => spawned, Err(error) => { let detail = error.error.to_string(); - self.store.finish_backend_job_attempt( - &self.config.workspace_id, + self.finish_backend_job_attempt( &request.job_id, &reservation.attempt.attempt_id, BackendJobAttemptState::Failed, "dispatch_error", &detail, - &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), )?; return Err(error.error); } @@ -3437,14 +3436,12 @@ impl WorkspaceApi { .first() .map(|diagnostic| diagnostic.message.as_str()) .unwrap_or("embedded Runtime rejected Backend Job Worker dispatch"); - self.store.finish_backend_job_attempt( - &self.config.workspace_id, + self.finish_backend_job_attempt( &request.job_id, &reservation.attempt.attempt_id, BackendJobAttemptState::Failed, "dispatch_rejected", detail, - &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), )?; return Err(Error::RuntimeOperationFailed { runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), @@ -3505,14 +3502,12 @@ impl WorkspaceApi { Ok(submitted) => submitted, Err(error) => { let detail = error.message(); - self.store.finish_backend_job_attempt( - &self.config.workspace_id, + self.finish_backend_job_attempt( &request.job_id, &reservation.attempt.attempt_id, BackendJobAttemptState::Unknown, "input_outcome_unknown", &detail, - &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), )?; return Err(error.into_error()); } @@ -3531,14 +3526,12 @@ impl WorkspaceApi { (BackendJobAttemptState::Failed, "input_rejected") } }; - self.store.finish_backend_job_attempt( - &self.config.workspace_id, + self.finish_backend_job_attempt( &request.job_id, &reservation.attempt.attempt_id, state, category, detail, - &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), )?; return Err(Error::RuntimeOperationFailed { runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), @@ -3547,14 +3540,12 @@ impl WorkspaceApi { }); } if submitted.runtime_run_id.as_deref() != Some(runtime_run_id.as_str()) { - self.store.finish_backend_job_attempt( - &self.config.workspace_id, + self.finish_backend_job_attempt( &request.job_id, &reservation.attempt.attempt_id, BackendJobAttemptState::Unknown, "runtime_run_mismatch", "embedded Runtime accepted Backend Job input without matching tracked run evidence", - &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), )?; return Err(Error::RuntimeOperationFailed { runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), @@ -3672,6 +3663,341 @@ impl WorkspaceApi { } } + fn finish_backend_job_attempt( + &self, + job_id: &str, + attempt_id: &str, + state: BackendJobAttemptState, + failure_category: &str, + failure_detail: &str, + ) -> Result { + let attempt = self.store.finish_backend_job_attempt( + &self.config.workspace_id, + job_id, + attempt_id, + state, + failure_category, + failure_detail, + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + )?; + let _ = self.schedule_backend_job_worker_cleanup(&attempt); + Ok(attempt) + } + + fn schedule_backend_job_worker_cleanup(&self, attempt: &BackendJobAttemptRecord) -> bool { + if !attempt.state.terminal() + || attempt.worker.is_none() + || !matches!( + attempt.worker_cleanup_state, + Some(BackendJobWorkerCleanupState::Pending | BackendJobWorkerCleanupState::Failed) + ) + { + return false; + } + let Ok(handle) = tokio::runtime::Handle::try_current() else { + tracing::warn!( + job_id = %attempt.job_id, + attempt_id = %attempt.attempt_id, + "Backend Job Worker cleanup remains durable because no Tokio runtime is available" + ); + return false; + }; + let (attempt, claimed) = match self.store.claim_backend_job_worker_cleanup( + &self.config.workspace_id, + &attempt.job_id, + &attempt.attempt_id, + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + ) { + Ok(claim) => claim, + Err(error) => { + tracing::warn!( + %error, + job_id = %attempt.job_id, + attempt_id = %attempt.attempt_id, + "failed to claim durable Backend Job Worker cleanup" + ); + return false; + } + }; + if !claimed { + return false; + } + let waits_for_run_teardown = attempt.state == BackendJobAttemptState::Completed + || attempt.failure_category.as_deref() == Some("invalid_result"); + if waits_for_run_teardown { + let api = self.clone(); + let cleanup_handle = handle.clone(); + handle.spawn(async move { + if let Err(error) = api.wait_for_backend_job_worker_run_teardown(&attempt).await { + tracing::warn!( + %error, + job_id = %attempt.job_id, + attempt_id = %attempt.attempt_id, + "Backend Job Worker Run teardown fence failed" + ); + return; + } + api.spawn_claimed_backend_job_worker_cleanup(&cleanup_handle, attempt); + }); + } else { + self.spawn_claimed_backend_job_worker_cleanup(&handle, attempt); + } + true + } + + fn spawn_claimed_backend_job_worker_cleanup( + &self, + handle: &tokio::runtime::Handle, + attempt: BackendJobAttemptRecord, + ) { + let api = self.clone(); + let job_id = attempt.job_id.clone(); + let attempt_id = attempt.attempt_id.clone(); + handle.spawn_blocking(move || { + let cleanup = || -> Result<()> { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|error| Error::Store(error.to_string()))? + .block_on(api.cleanup_backend_job_attempt_worker(attempt)) + }; + if let Err(error) = cleanup() { + tracing::warn!(%error, %job_id, %attempt_id, "Backend Job Worker cleanup failed"); + } + }); + } + + async fn wait_for_backend_job_worker_run_teardown( + &self, + attempt: &BackendJobAttemptRecord, + ) -> Result<()> { + let worker = attempt.worker.as_ref().ok_or_else(|| { + Error::Store("claimed Backend Job Worker cleanup lost its binding".to_string()) + })?; + let Some(runtime_run_id) = attempt.runtime_run_id.as_deref() else { + // Attempts created before tracked Runtime submissions can only be + // recovered after a Backend restart, which has already ended their + // in-process result response and Run. Canonical stop/remove remains + // the safety boundary for those legacy leftovers. + return Ok(()); + }; + loop { + match self.runtime.worker(worker) { + Ok(detail) + if detail.state.eq_ignore_ascii_case("stopped") + || detail.execution_reconstructed + || detail.worker_state.as_ref().is_some_and(|snapshot| { + snapshot.state == protocol::WorkerState::Idle + && snapshot.last_finished_submission_request_id.as_deref() + == Some(runtime_run_id) + }) => + { + return Ok(()); + } + Ok(_) => { + // In this process incarnation the result capability runs inside + // this tracked Submit. The controller publishes the exact request + // id only after the invocation returns. A Worker reconstructed + // from durable state is handled above instead: opening the + // Worker store exclusively proves its prior process and any + // pre-marker Run are gone, even if execution restoration has + // not yet produced a fresh live snapshot. + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + Err(RuntimeRegistryError::UnknownWorker { .. }) => return Ok(()), + Err(error) => { + self.store.finish_backend_job_worker_cleanup( + &self.config.workspace_id, + &attempt.job_id, + &attempt.attempt_id, + BackendJobWorkerCleanupState::Failed, + Some("backend_job_cleanup_run_state_unavailable"), + Some(&error.message()), + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + )?; + return Err(error.into_error()); + } + } + } + } + + fn recover_backend_job_worker_cleanups(&self) -> Result<()> { + let page_limit = usize::from(ABSOLUTE_MAX_CONCURRENT_JOBS); + let mut attempts = self.store.recover_backend_job_worker_cleanups( + &self.config.workspace_id, + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + page_limit, + )?; + loop { + if attempts.is_empty() { + return Ok(()); + } + let scheduled = attempts + .iter() + .filter(|attempt| self.schedule_backend_job_worker_cleanup(attempt)) + .count(); + if attempts.len() < page_limit || scheduled == 0 { + return Ok(()); + } + attempts = self + .store + .list_backend_job_worker_cleanups(&self.config.workspace_id, page_limit)?; + } + } + + async fn cleanup_backend_job_attempt_worker( + &self, + attempt: BackendJobAttemptRecord, + ) -> Result<()> { + let job_id = attempt.job_id.as_str(); + let attempt_id = attempt.attempt_id.as_str(); + let worker = attempt.worker.clone().ok_or_else(|| { + Error::Store("claimed Backend Job Worker cleanup lost its binding".to_string()) + })?; + + let fail = |category: &str, detail: String| -> Result<()> { + self.store.finish_backend_job_worker_cleanup( + &self.config.workspace_id, + job_id, + attempt_id, + BackendJobWorkerCleanupState::Failed, + Some(category), + Some(&detail), + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + )?; + Err(Error::RuntimeOperationFailed { + runtime_id: worker.runtime_id.clone(), + code: category.to_string(), + message: detail, + }) + }; + let complete = || -> Result<()> { + self.store.finish_backend_job_worker_cleanup( + &self.config.workspace_id, + job_id, + attempt_id, + BackendJobWorkerCleanupState::Completed, + None, + None, + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + )?; + Ok(()) + }; + + let detail = match self.runtime.worker(&worker) { + Ok(detail) => detail, + Err(RuntimeRegistryError::UnknownWorker { .. }) => { + if self + .store + .get_worker_registry(&self.config.workspace_id, &worker)? + .is_none() + { + return complete(); + } + // A canonical removal may have deleted the Runtime object before + // its catalog commit. Let the existing durable removal operation + // recover that midpoint; without such an operation this remains + // a diagnosable retryable failure rather than a direct DB delete. + let response = WorkerRemovalService::new(self) + .execute_target_removal( + self.runtime.as_ref(), + &worker, + &format!("Terminal Backend Job attempt {attempt_id}"), + None, + ) + .await + .map_err(|message| Error::RuntimeOperationFailed { + runtime_id: worker.runtime_id.clone(), + code: "backend_job_cleanup_removal_failed".to_string(), + message, + }); + return match response { + Ok(response) if response.status == StatusCode::OK.as_u16() => complete(), + Ok(response) => { + fail("backend_job_cleanup_runtime_worker_missing", response.body) + } + Err(error) => fail("backend_job_cleanup_removal_failed", error.to_string()), + }; + } + Err(error) => { + return fail("backend_job_cleanup_runtime_unavailable", error.message()); + } + }; + if !detail.state.eq_ignore_ascii_case("stopped") { + match self.runtime.stop_worker( + &worker, + WorkerLifecycleRequest { + reason: Some(format!( + "Backend Job attempt {attempt_id} reached terminal state" + )), + ticket_assignment: None, + }, + ) { + Ok(result) if result.state == InternalWorkerOperationState::Accepted => {} + Ok(result) => { + return fail( + "backend_job_cleanup_stop_rejected", + runtime_diagnostics_message(&result.diagnostics), + ); + } + Err(error) => { + return fail("backend_job_cleanup_stop_failed", error.message()); + } + } + } + match self.runtime.worker(&worker) { + Ok(detail) if detail.state.eq_ignore_ascii_case("stopped") => {} + Ok(detail) => { + return fail( + "backend_job_cleanup_stop_unconfirmed", + format!( + "Runtime reported Worker state `{}` after accepting stop", + detail.state + ), + ); + } + Err(error) => { + return fail( + "backend_job_cleanup_stop_confirmation_failed", + error.message(), + ); + } + } + + let response = WorkerRemovalService::new(self) + .execute_target_removal( + self.runtime.as_ref(), + &worker, + &format!("Terminal Backend Job attempt {attempt_id}"), + None, + ) + .await + .map_err(|message| Error::RuntimeOperationFailed { + runtime_id: worker.runtime_id.clone(), + code: "backend_job_cleanup_removal_failed".to_string(), + message, + }); + let response = match response { + Ok(response) => response, + Err(error) => return fail("backend_job_cleanup_removal_failed", error.to_string()), + }; + if response.status == StatusCode::OK.as_u16() { + return complete(); + } + let runtime_absent = matches!( + self.runtime.worker(&worker), + Err(RuntimeRegistryError::UnknownWorker { .. }) + ); + let catalog_absent = self + .store + .get_worker_registry(&self.config.workspace_id, &worker)? + .is_none(); + if runtime_absent && catalog_absent { + return complete(); + } + fail("backend_job_cleanup_removal_rejected", response.body) + } + /// Reconcile durable non-terminal attempts after Backend restart. Reserved /// attempts are replay-dispatched through their deterministic allocation; /// dispatched attempts keep running only while their bound Worker remains @@ -3701,26 +4027,22 @@ impl WorkspaceApi { } } BackendJobAttemptState::Dispatching => { - self.store.finish_backend_job_attempt( - &self.config.workspace_id, + self.finish_backend_job_attempt( &attempt.job_id, &attempt.attempt_id, BackendJobAttemptState::Unknown, "recovery_dispatch_claim_unknown", "a Backend Job dispatch claim could not be safely replayed after restart", - &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), )?; } BackendJobAttemptState::Dispatched => { if attempt.runtime_run_id.is_none() { - self.store.finish_backend_job_attempt( - &self.config.workspace_id, + self.finish_backend_job_attempt( &attempt.job_id, &attempt.attempt_id, BackendJobAttemptState::Unknown, "recovery_runtime_run_unknown", "the Runtime accepted Worker dispatch without durable run evidence", - &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), )?; continue; } @@ -3729,14 +4051,12 @@ impl WorkspaceApi { .as_ref() .is_some_and(|worker| self.runtime.worker(worker).is_ok()); if !worker_known { - self.store.finish_backend_job_attempt( - &self.config.workspace_id, + self.finish_backend_job_attempt( &attempt.job_id, &attempt.attempt_id, BackendJobAttemptState::Unknown, "recovery_worker_unknown", "the bound Runtime Worker was not recoverable after Backend restart", - &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), )?; } else { self.schedule_backend_job_timeout( @@ -3751,6 +4071,7 @@ impl WorkspaceApi { | BackendJobAttemptState::Unknown => {} } } + self.recover_backend_job_worker_cleanups()?; self.drain_reserved_backend_jobs(); Ok(()) } @@ -3766,15 +4087,13 @@ impl WorkspaceApi { return; }; let api = self.clone(); - let store = api.store.clone(); - let runtime = api.runtime.clone(); - let workspace_id = api.config.workspace_id.clone(); let job_id = job_id.to_string(); let attempt_id = attempt_id.to_string(); handle.spawn(async move { tokio::time::sleep(delay).await; let Ok(Some(attempt)) = - store.get_backend_job_attempt(&workspace_id, &job_id, &attempt_id) + api.store + .get_backend_job_attempt(&api.config.workspace_id, &job_id, &attempt_id) else { return; }; @@ -3783,27 +4102,16 @@ impl WorkspaceApi { } // Fence result acceptance before stopping the Worker so a late // completion cannot turn a timed-out attempt into success. - if store + if api .finish_backend_job_attempt( - &workspace_id, &job_id, &attempt_id, BackendJobAttemptState::Failed, "timeout", "Backend Job attempt exceeded its durable deadline", - &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), ) .is_ok() { - if let Some(worker) = attempt.worker { - let _ = runtime.stop_worker( - &worker, - WorkerLifecycleRequest { - reason: Some("Backend Job attempt timed out".to_string()), - ticket_assignment: None, - }, - ); - } api.drain_reserved_backend_jobs(); } }); @@ -3848,14 +4156,12 @@ impl WorkspaceApi { { let detail = error.to_string(); if attempt.state == BackendJobAttemptState::Dispatched { - self.store.finish_backend_job_attempt( - &self.config.workspace_id, + self.finish_backend_job_attempt( &submission.job_id, &submission.attempt_id, BackendJobAttemptState::Failed, "invalid_result", &detail, - &accepted_at, )?; self.drain_reserved_backend_jobs(); } @@ -3871,6 +4177,7 @@ impl WorkspaceApi { if let Some(target) = acceptance.job.request.notification_target.as_ref() { self.deliver_backend_job_result(&acceptance, target); } + let _ = self.schedule_backend_job_worker_cleanup(&acceptance.attempt); self.drain_reserved_backend_jobs(); Ok(acceptance) } @@ -5468,9 +5775,6 @@ fn generated_workspace_contract_router(service: ServerApiContractService) -> Rou .merge(server_api::server_api_axum::subjektiv_subject_retire( service.clone(), )) - .merge(server_api::server_api_axum::subjektiv_subject_worker_start( - service.clone(), - )) .merge(server_api::server_api_axum::subjektiv_stage_candidate( service.clone(), )) @@ -7235,27 +7539,6 @@ impl server_api::ServerApi for ServerApiContractService { .map_err(ApiError::into_repository_api_error) } - async fn subjektiv_subject_worker_start( - &self, - context: server_api::ServerRequestContext, - workspace_id: String, - subject_id: String, - request: server_api::SubjektivSubjectWorkerStartRequest, - ) -> std::result::Result { - scoped_start_subjektiv_subject_worker( - State(self.workspace_api()?.clone()), - AxumPath(ScopedSubjektivSubjectPath { - workspace_id, - subject_id, - }), - contract_request_headers(&context)?, - Json(request), - ) - .await - .map(|Json(response)| response) - .map_err(ApiError::into_repository_api_error) - } - async fn subjektiv_stage_candidate( &self, context: server_api::ServerRequestContext, @@ -10292,6 +10575,72 @@ impl server_api::ServerApi for ServerApiContractService { } } +async fn scoped_worker_session_attachment( + State(api): State, + AxumPath((workspace_id, runtime_id, worker_id, session_id, attachment_id)): AxumPath<( + String, + String, + String, + String, + String, + )>, + Extension(_actor): Extension, +) -> Response { + if workspace_id != api.config.workspace_id { + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({"code": "workspace_not_found"})), + ) + .into_response(); + } + let worker = RuntimeWorkerRef::new(runtime_id, worker_id); + match api + .runtime + .worker_session_attachment(&workspace_id, &worker, &session_id, &attachment_id) + { + Ok(attachment) => { + let Ok(content_type) = HeaderValue::from_str(&attachment.media_type) else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({"code": "session_attachment_media_type_invalid"})), + ) + .into_response(); + }; + ( + [ + (CONTENT_TYPE, content_type), + ( + CACHE_CONTROL, + HeaderValue::from_static("private, max-age=3600"), + ), + ], + attachment.data, + ) + .into_response() + } + Err(crate::hosts::RuntimeRegistryError::RuntimeOperationFailed { + code, message, .. + }) => { + let status = match code.as_str() { + "session_attachment_expired" => StatusCode::GONE, + "session_attachment_not_found" => StatusCode::NOT_FOUND, + "session_attachment_too_large" => StatusCode::PAYLOAD_TOO_LARGE, + _ => StatusCode::BAD_GATEWAY, + }; + ( + status, + Json(serde_json::json!({"code": code, "message": message})), + ) + .into_response() + } + Err(_) => ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({"code": "session_attachment_not_found"})), + ) + .into_response(), + } +} + fn build_inner_router(api: WorkspaceApi) -> Router { let contract_service = ServerApiContractService::Workspace(api.clone()); let auth = generated_auth_contract_router(contract_service.clone()); @@ -10320,6 +10669,10 @@ fn build_inner_router(api: WorkspaceApi) -> Router { _, _, >(workspace, scoped_worker_protocol_ws) + .route( + "/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/sessions/{session_id}/attachments/{attachment_id}", + get(scoped_worker_session_attachment), + ) .route( "/api/w/{workspace_id}/workers/self/backend-job-result", post(scoped_submit_backend_job_result), @@ -16780,7 +17133,7 @@ fn resident_summary_output( } } -const SUBJEKTIV_SINGLETON_PREFIX: &str = "subjektiv:"; +const SUBJEKTIV_SINGLETON_PREFIX: &str = crate::subjektiv::SUBJECT_WORKER_SINGLETON_PREFIX; const SUBJEKTIV_CONSOLIDATION_SINGLETON_PREFIX: &str = crate::hosts::SUBJEKTIV_CONSOLIDATION_SINGLETON_PREFIX; const SUBJEKTIV_MEMORY_CONSOLIDATION_PROFILE: &str = "subjektiv-memory-consolidation"; @@ -16821,15 +17174,6 @@ fn subjektiv_scope_from_singleton_key(key: &str) -> ApiResult<(&str, SubjektivWo Ok((subject_id, authority)) } -fn subjektiv_singleton_key(subject_id: &str) -> ApiResult { - if subject_id.trim().is_empty() || subject_id.chars().any(char::is_control) { - return Err(Error::InvalidInput("subjektiv subject id is invalid".to_string()).into()); - } - let key = format!("{SUBJEKTIV_SINGLETON_PREFIX}{subject_id}"); - crate::store::validate_worker_singleton_key(&key)?; - Ok(key) -} - fn is_dedicated_subjektiv_consolidation_worker( worker: &InternalWorkerSummary, workspace_id: &str, @@ -16943,7 +17287,7 @@ fn subjektiv_subject_response( api: &WorkspaceApi, subject: crate::subjektiv::SubjectRecord, ) -> server_api::SubjektivSubjectResponse { - let current_worker = subjektiv_singleton_key(&subject.id) + let current_worker = crate::subjektiv::subject_worker_singleton_key(&subject.id) .ok() .and_then(|key| { api.store @@ -17199,36 +17543,6 @@ async fn scoped_retire_subjektiv_subject( Ok(Json(subjektiv_subject_response(&api, subject))) } -async fn scoped_start_subjektiv_subject_worker( - State(api): State, - AxumPath(path): AxumPath, - headers: HeaderMap, - Json(request): Json, -) -> ApiResult> { - validate_workspace_scope(&api, &path.workspace_id)?; - let subject = open_subjektiv_store(&api)? - .subject(&path.subject_id) - .map_err(|error| Error::Store(error.to_string()))? - .ok_or_else(|| Error::SubjektivSubjectNotFound(path.subject_id.clone()))?; - if subject.state != crate::subjektiv::SubjectState::Active { - return Err(Error::InvalidInput(format!( - "subjektiv subject `{}` is retired", - path.subject_id - )) - .into()); - } - let mut worker = request.worker; - if worker.singleton_key.is_some() { - return Err(Error::InvalidInput( - "subjektiv subject Worker start owns singleton selection; singleton_key must be omitted" - .to_string(), - ) - .into()); - } - worker.singleton_key = Some(subjektiv_singleton_key(&path.subject_id)?); - create_workspace_worker_inner(api, headers, worker, None, true).await -} - async fn scoped_stage_subjektiv_candidate( State(api): State, AxumPath(path): AxumPath, @@ -21529,14 +21843,9 @@ async fn spawn_known_worker( operation_id: scoped_worker_control_operation_id(&controller, &operation_id), input_fingerprint, }); - let response = create_workspace_worker_inner( - api.clone(), - headers, - request, - resolved_control_operation, - false, - ) - .await?; + let response = + create_workspace_worker_inner(api.clone(), headers, request, resolved_control_operation) + .await?; if let Err(error) = api .store .create_worker_control_grant(&WorkerControlGrantRecord { @@ -27110,7 +27419,7 @@ async fn create_workspace_worker( headers: HeaderMap, Json(request): Json, ) -> ApiResult> { - create_workspace_worker_inner(api, headers, request, None, false).await + create_workspace_worker_inner(api, headers, request, None).await } async fn create_workspace_worker_inner( @@ -27118,33 +27427,19 @@ async fn create_workspace_worker_inner( headers: HeaderMap, request: CreateWorkspaceWorkerRequest, resolved_control_operation: Option, - resolved_subjektiv_attached: bool, ) -> ApiResult> { let CreateWorkspaceWorkerRequest { runtime_id, display_name, - singleton_key, + mut singleton_key, profile, ticket_assignment, initial_submit, workdir_attachments, + feature_connections, control_operation_id: _, } = request; - if resolved_subjektiv_attached { - let singleton_key = singleton_key.as_deref().ok_or_else(|| { - Error::InvalidInput( - "subject-attached Worker launch requires a Backend-owned singleton key".to_string(), - ) - })?; - if !singleton_key.starts_with(SUBJEKTIV_SINGLETON_PREFIX) { - return Err(Error::InvalidInput( - "subject-attached Worker launch requires a subject singleton key".to_string(), - ) - .into()); - } - } else { - validate_caller_worker_singleton_key(singleton_key.as_deref())?; - } + let source = optional_worker_mutation_source(&api, &api.config.workspace_id, &headers)?; let config_state = api .config_store .load_workspace_config(&api.config.workspace_id)? @@ -27185,6 +27480,34 @@ async fn create_workspace_worker_inner( &profile, prompt_catalog.as_ref(), )?; + let resolved_subjektiv_attached = match feature_connections.subjektiv { + None => { + validate_caller_worker_singleton_key(singleton_key.as_deref())?; + false + } + Some(connection) => { + if singleton_key.is_some() { + return Err(Error::InvalidInput( + "singleton_key must be omitted when a subjektiv subject connection is requested" + .to_string(), + ) + .into()); + } + let profile_manifest = + crate::profile_settings::resolve_profile_manifest_from_config_bundle( + resolved_config_bundle.as_ref().ok_or_else(|| { + Error::Config("resolved Worker Profile bundle is missing".to_string()) + })?, + &profile, + )?; + let attachment = open_subjektiv_store(&api)? + .authorize_worker_attachment(&connection.subject_id, &profile_manifest) + .map_err(subjektiv_store_error)?; + crate::store::validate_worker_singleton_key(&attachment.singleton_key)?; + singleton_key = Some(attachment.singleton_key); + true + } + }; let display_name = sanitize_worker_display_name(&display_name).ok_or_else(|| { settings_bad_request( "invalid_worker_display_name", @@ -27195,7 +27518,6 @@ async fn create_workspace_worker_inner( return Err(Error::ReservedWorkerName(display_name).into()); } validate_worker_initial_submit(&initial_submit)?; - let source = optional_worker_mutation_source(&api, &api.config.workspace_id, &headers)?; reject_orchestrator_generic_flow_spawn( &api, source.as_ref(), @@ -29771,32 +30093,55 @@ fn worker_launch_options_response(api: &WorkspaceApi) -> ApiResult>>()?; Ok(WorkerLaunchOptionsResponse { workspace_id: api.config.workspace_id.clone(), runtimes, - default_profile: profile_settings.default_profile, + default_profile: profile_projection.settings.default_profile.clone(), profiles, repositories: working_directory_repository_options(api), working_directories: available_working_directory_summaries(api) @@ -30033,6 +30378,7 @@ fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> InternalWorker singleton_key: None, tags: Vec::new(), state: "missing".to_string(), + execution_reconstructed: false, worker_state: None, last_seen_at: Some(record.updated_at.clone()), pinned: record.retention_state == "pinned", @@ -32729,6 +33075,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -33258,6 +33605,15 @@ mod tests { } let options = worker_launch_options_response(&api).unwrap(); + assert!( + options + .profiles + .iter() + .find(|profile| profile.id == "builtin:companion") + .unwrap() + .feature_connections + .subjektiv + ); let available = options .working_directories .iter() @@ -33306,6 +33662,7 @@ mod tests { relative_cwd: None, }, ], + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -33543,6 +33900,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -34076,6 +34434,7 @@ mod tests { }, availability: protocol::subscription::SubscriptionWorkerAvailability::Observed, state: "idle".to_string(), + execution_reconstructed: false, worker_state: None, last_seen_at: None, pinned: false, @@ -36389,6 +36748,7 @@ mod tests { selector: "builtin:coder-review".to_string(), }], workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -36423,6 +36783,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -36458,6 +36819,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -36547,6 +36909,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -36805,6 +37168,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -36864,6 +37228,7 @@ mod tests { "working_directory_id": fixture.read_only_workdir_id, } ], + "feature_connections": {}, "control_operation_id": operation_id, }) } @@ -37287,6 +37652,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -37328,6 +37694,7 @@ mod tests { "working_directory_id": read_only_workdir_id, } ], + "feature_connections": {}, "control_operation_id": "guarded-strict-remote-success", }); assert!(payload.get("working_directory").is_none()); @@ -37580,6 +37947,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -37612,6 +37980,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: Some("control-spawn-retry".to_string()), }; @@ -37687,6 +38056,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }; @@ -37845,6 +38215,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -37943,6 +38314,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }; let Json(controller) = create_workspace_worker( @@ -38555,6 +38927,15 @@ mod tests { ); let memory_root = format!("{subject_root}/memories/{}", memory.id); + request_json_authenticated( + app.clone(), + "POST", + &format!("{subject_root}/worker"), + Some(serde_json::json!({})), + &owner_token, + StatusCode::METHOD_NOT_ALLOWED, + ) + .await; request_json( app.clone(), "GET", @@ -38976,6 +39357,107 @@ mod tests { assert_eq!(snapshot.memory_refs[0].revision, expected_ref.revision); } + #[tokio::test] + async fn ordinary_worker_launch_validates_optional_subjektiv_connection() { + let workspace = tempfile::tempdir().unwrap(); + init_clean_git_workspace(workspace.path()); + let api = test_api(workspace.path()).await; + let store = open_subjektiv_store(&api).unwrap(); + let active = store + .create_subject(crate::subjektiv::SubjectRole::new("companion").unwrap()) + .unwrap(); + let retired = store + .create_subject(crate::subjektiv::SubjectRole::new("retired").unwrap()) + .unwrap(); + store.retire_subject(&retired.id).unwrap(); + + let request = |subject_id: Option<&str>, profile: &str, singleton_key: Option<&str>| { + CreateWorkspaceWorkerRequest { + runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), + display_name: "Ordinary launch".to_string(), + singleton_key: singleton_key.map(ToOwned::to_owned), + profile: Some(profile.to_string()), + ticket_assignment: None, + initial_submit: Vec::new(), + workdir_attachments: Vec::new(), + feature_connections: server_api::WorkspaceWorkerFeatureConnectionsRequest { + subjektiv: subject_id.map(|subject_id| { + server_api::SubjektivWorkerConnectionRequest { + subject_id: subject_id.to_string(), + } + }), + }, + control_operation_id: None, + } + }; + + let Json(ordinary) = create_workspace_worker( + State(api.clone()), + HeaderMap::new(), + Json(request(None, "builtin:companion", None)), + ) + .await + .unwrap(); + assert!(ordinary.worker.singleton_key.is_none()); + + for (candidate, profile, singleton_key, expected_status) in [ + ( + "missing-subject", + "builtin:companion", + None, + StatusCode::NOT_FOUND, + ), + ( + retired.id.as_str(), + "builtin:companion", + None, + StatusCode::BAD_REQUEST, + ), + ( + active.id.as_str(), + "builtin:standalone", + None, + StatusCode::BAD_REQUEST, + ), + ( + active.id.as_str(), + "builtin:companion", + Some("caller-key"), + StatusCode::BAD_REQUEST, + ), + ] { + let response = create_workspace_worker( + State(api.clone()), + HeaderMap::new(), + Json(request(Some(candidate), profile, singleton_key)), + ) + .await + .unwrap_err() + .into_response(); + assert_eq!(response.status(), expected_status, "candidate={candidate}"); + } + + let Json(first) = create_workspace_worker( + State(api.clone()), + HeaderMap::new(), + Json(request(Some(&active.id), "builtin:companion", None)), + ) + .await + .unwrap(); + let Json(retried) = create_workspace_worker( + State(api.clone()), + HeaderMap::new(), + Json(request(Some(&active.id), "builtin:companion", None)), + ) + .await + .unwrap(); + assert_eq!(retried.worker_id, first.worker_id); + assert_eq!( + first.worker.singleton_key.as_deref(), + Some(format!("subjektiv:{}", active.id).as_str()) + ); + } + #[tokio::test] async fn subjektiv_subject_worker_and_staging_derive_scope_from_singleton_authority() { let workspace = tempfile::tempdir().unwrap(); @@ -38996,23 +39478,25 @@ mod tests { .unwrap() .create_subject(crate::subjektiv::SubjectRole::new("companion").unwrap()) .unwrap(); - let request = server_api::SubjektivSubjectWorkerStartRequest { - worker: CreateWorkspaceWorkerRequest { - runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), - display_name: "Subject Worker".to_string(), - singleton_key: None, - profile: Some("builtin:companion".to_string()), - ticket_assignment: None, - initial_submit: Vec::new(), - workdir_attachments: Vec::new(), - control_operation_id: None, + let request = CreateWorkspaceWorkerRequest { + runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), + display_name: "Subject Worker".to_string(), + singleton_key: None, + profile: Some("builtin:companion".to_string()), + ticket_assignment: None, + initial_submit: Vec::new(), + workdir_attachments: Vec::new(), + feature_connections: server_api::WorkspaceWorkerFeatureConnectionsRequest { + subjektiv: Some(server_api::SubjektivWorkerConnectionRequest { + subject_id: subject.id.clone(), + }), }, + control_operation_id: None, }; - let Json(created) = scoped_start_subjektiv_subject_worker( + let Json(created) = scoped_create_workspace_worker( State(api.clone()), - AxumPath(ScopedSubjektivSubjectPath { + AxumPath(ScopedWorkspacePath { workspace_id: TEST_WORKSPACE_ID.to_string(), - subject_id: subject.id.clone(), }), HeaderMap::new(), Json(request), @@ -40320,6 +40804,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -40339,6 +40824,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -40423,6 +40909,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -41182,6 +41669,7 @@ mod tests { Option>, >, input_request_ids: std::sync::Mutex>>, + accept_restores: std::sync::atomic::AtomicBool, inputs: std::sync::Mutex>, protocol_methods: std::sync::Mutex>, @@ -41206,6 +41694,7 @@ mod tests { input_failure: std::sync::Mutex::new(None), input_hook: std::sync::Mutex::new(None), input_request_ids: std::sync::Mutex::new(Vec::new()), + accept_restores: std::sync::atomic::AtomicBool::new(false), inputs: std::sync::Mutex::new(Vec::new()), protocol_methods: std::sync::Mutex::new(Vec::new()), } @@ -41217,6 +41706,11 @@ mod tests { *self.input_failure.lock().unwrap() = Some(message.into()); } + fn accept_restores(&self) { + self.accept_restores + .store(true, std::sync::atomic::Ordering::SeqCst); + } + fn before_input_returns( &self, hook: impl FnOnce(&worker_runtime::identity::WorkerRef) + Send + 'static, @@ -41224,6 +41718,24 @@ mod tests { *self.input_hook.lock().unwrap() = Some(Box::new(hook)); } + fn publish_worker_state( + &self, + worker: &RuntimeWorkerRef, + snapshot: protocol::WorkerStateSnapshot, + ) { + let context = self + .contexts + .lock() + .unwrap() + .iter() + .find(|(worker_ref, _)| worker_ref.worker_id.to_string() == worker.worker_id) + .map(|(_, context)| context.clone()) + .expect("execution context"); + context + .publish_protocol_event(protocol::Event::WorkerState { snapshot }) + .unwrap(); + } + fn take_input_request_ids(&self) -> Vec> { std::mem::take( &mut *self @@ -41327,6 +41839,43 @@ mod tests { } } + fn restore_worker( + &self, + request: worker_runtime::execution::WorkerExecutionRestoreRequest, + ) -> worker_runtime::execution::WorkerExecutionSpawnResult { + if !self + .accept_restores + .load(std::sync::atomic::Ordering::SeqCst) + { + return worker_runtime::execution::WorkerExecutionSpawnResult::Rejected( + worker_runtime::execution::WorkerExecutionResult::rejected( + worker_runtime::execution::WorkerExecutionOperation::Restore, + "deterministic test backend restore disabled", + ), + ); + } + let workdir_attachments = request + .workdir_attachments + .iter() + .map(|(alias, binding)| WorkingDirectoryAttachmentStatus { + alias: alias.clone(), + working_directory: binding.status(), + }) + .collect(); + self.contexts + .lock() + .unwrap() + .insert(request.worker_ref.clone(), request.context); + worker_runtime::execution::WorkerExecutionSpawnResult::Connected { + handle: worker_runtime::execution::WorkerExecutionHandle::new( + request.worker_ref, + self.backend_id(), + ), + worker_state: protocol::WorkerStateSnapshot::initial(), + workdir_attachments, + } + } + fn dispatch_method( &self, handle: &worker_runtime::execution::WorkerExecutionHandle, @@ -41437,11 +41986,15 @@ mod tests { } else if let Some(submission_request_id) = submission_request_id { worker_runtime::execution::WorkerExecutionResult::accepted_submission( worker_runtime::execution::WorkerExecutionOperation::Input, - submission_request_id, + submission_request_id.clone(), uuid::Uuid::now_v7().to_string(), protocol::SubmissionDisposition::Started, ) - .with_worker_state(protocol::WorkerStateSnapshot::initial()) + .with_worker_state(protocol::WorkerStateSnapshot { + last_command_id: 0, + last_finished_submission_request_id: Some(submission_request_id), + state: protocol::WorkerState::Idle, + }) } else { worker_runtime::execution::WorkerExecutionResult::accepted( worker_runtime::execution::WorkerExecutionOperation::Input, @@ -41580,6 +42133,27 @@ mod tests { config } + fn seed_worker_session_for_cleanup(root: &Path, worker: &RuntimeWorkerRef) { + let worker_root = root + .join(".test-embedded-runtime-store/workers") + .join(&worker.worker_id); + fs::create_dir_all(worker_root.join("session/segments")).unwrap(); + fs::write( + worker_root.join("session/session.json"), + serde_json::to_vec_pretty(&json!({ + "schema_version": 1, + "session_id": format!("backend-job-cleanup-{}", worker.worker_id) + })) + .unwrap(), + ) + .unwrap(); + fs::write( + worker_root.join("session/segments/segment-a.jsonl"), + b"backend job retained session evidence\n", + ) + .unwrap(); + } + fn spawn_ticket_check_source(api: &WorkspaceApi, name: &str) -> RuntimeWorkerRef { api.spawn_workspace_worker( EMBEDDED_WORKER_RUNTIME_ID, @@ -41664,6 +42238,30 @@ mod tests { panic!("Ticket item checker attempt remained reserved") } + async fn wait_for_backend_job_worker_cleanup( + api: &WorkspaceApi, + job_id: &str, + attempt_id: &str, + expected: BackendJobWorkerCleanupState, + ) -> crate::backend_job::BackendJobAttemptRecord { + let mut last = None; + for _ in 0..500 { + let attempt = api + .store + .get_backend_job_attempt(TEST_WORKSPACE_ID, job_id, attempt_id) + .unwrap() + .unwrap(); + if attempt.worker_cleanup_state == Some(expected) { + return attempt; + } + last = Some(attempt); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + panic!( + "Backend Job Worker cleanup for {job_id}/{attempt_id} did not reach {expected:?}: {last:?}" + ) + } + #[tokio::test] async fn worker_ticket_create_returns_before_checker_and_delivers_grounded_advisory() { let temp = tempfile::tempdir().unwrap(); @@ -42425,6 +43023,392 @@ mod tests { assert_eq!(accepted.job.state, BackendJobState::Completed); } + #[tokio::test] + async fn terminal_backend_job_cleanup_removes_only_bound_worker_and_retains_result_delivery() { + let temp = tempfile::tempdir().unwrap(); + let (api, execution) = test_api_with_recording_backend(temp.path()).await; + let notification_target = spawn_ticket_check_source(&api, "job-cleanup-target"); + let request = BackendJobRequest { + job_id: "runner-cleanup-success-1".to_string(), + purpose: "cleanup_contract_check".to_string(), + input_revision: "revision-1".to_string(), + input_ref: "test://runner/cleanup/1".to_string(), + input: serde_json::json!({"value": "immutable"}), + instruction: "Return a structured check result.".to_string(), + profile: "builtin:backend-job".to_string(), + source_worker: Some(notification_target.clone()), + notification_target: Some(notification_target.clone()), + limits: crate::backend_job::BackendJobLimits::default(), + }; + let dispatched = api.dispatch_backend_job(&request).unwrap(); + execution.take_inputs(); + execution.reject_inputs("notification unavailable"); + let worker = dispatched.attempt.worker.clone().unwrap(); + seed_worker_session_for_cleanup(temp.path(), &worker); + let mut subscriber = api.worker_projection.subscribe_ordered(10).unwrap(); + let _ = subscriber.take_snapshot(); + let submission = BackendJobResultSubmission { + job_id: request.job_id.clone(), + attempt_id: dispatched.attempt.attempt_id.clone(), + input_revision: request.input_revision.clone(), + result: serde_json::json!({"valid": true}), + }; + + let accepted = api.accept_backend_job_result(&worker, &submission).unwrap(); + assert_eq!(accepted.job.state, BackendJobState::Completed); + let cleaned = wait_for_backend_job_worker_cleanup( + &api, + &request.job_id, + &dispatched.attempt.attempt_id, + BackendJobWorkerCleanupState::Completed, + ) + .await; + assert_eq!(cleaned.result, Some(serde_json::json!({"valid": true}))); + assert!(matches!( + api.runtime.worker(&worker), + Err(RuntimeRegistryError::UnknownWorker { .. }) + )); + assert!( + !temp + .path() + .join(".test-embedded-runtime-store/workers") + .join(&worker.worker_id) + .exists(), + "Runtime Worker and Session storage must be removed" + ); + assert!( + api.store + .get_worker_registry(TEST_WORKSPACE_ID, &worker) + .unwrap() + .is_none(), + "canonical removal must delete the Backend catalog projection" + ); + tokio::time::timeout(std::time::Duration::from_secs(1), async { + loop { + let event = subscriber.recv().await.expect("projection event"); + if event.changes.iter().any(|change| { + matches!(change, crate::store::WorkerCatalogChange::Removed(removed) if removed == &worker) + }) { + return; + } + } + }) + .await + .expect("Backend Job cleanup must publish canonical Worker removal"); + assert!( + api.runtime.worker(¬ification_target).is_ok(), + "origin/notification Workers are not cleanup targets" + ); + assert!( + api.store + .get_worker_registry(TEST_WORKSPACE_ID, ¬ification_target) + .unwrap() + .is_some() + ); + let replay = api.accept_backend_job_result(&worker, &submission).unwrap(); + assert!( + replay.replayed, + "duplicate result remains idempotent after cleanup" + ); + let delivery_id = + crate::backend_job::delivery_id(&request.job_id, &dispatched.attempt.attempt_id); + let (delivery, claimed) = api + .store + .reserve_backend_job_delivery( + TEST_WORKSPACE_ID, + &delivery_id, + &request.job_id, + ¬ification_target, + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + ) + .unwrap(); + assert!(!claimed); + assert_eq!(delivery.state, BackendJobDeliveryState::Unknown); + assert_eq!( + delivery.failure_category.as_deref(), + Some("notification_outcome_unknown") + ); + } + + #[tokio::test] + async fn backend_job_cleanup_guard_failure_retries_old_attempt_without_touching_new_worker() { + let temp = tempfile::tempdir().unwrap(); + let (api, _execution) = test_api_with_recording_backend(temp.path()).await; + let request = BackendJobRequest { + job_id: "runner-cleanup-retry-1".to_string(), + purpose: "cleanup_retry_contract_check".to_string(), + input_revision: "revision-1".to_string(), + input_ref: "test://runner/cleanup-retry/1".to_string(), + input: serde_json::json!({"value": "immutable"}), + instruction: "Return a structured check result.".to_string(), + profile: "builtin:backend-job".to_string(), + source_worker: None, + notification_target: None, + limits: crate::backend_job::BackendJobLimits::default(), + }; + let first = api.dispatch_backend_job(&request).unwrap(); + let first_worker = first.attempt.worker.clone().unwrap(); + seed_worker_session_for_cleanup(temp.path(), &first_worker); + let _ = set_worker_retention( + api.clone(), + first_worker.runtime_id.clone(), + first_worker.worker_id.clone(), + true, + ) + .await + .unwrap(); + api.finish_backend_job_attempt( + &request.job_id, + &first.attempt.attempt_id, + BackendJobAttemptState::Failed, + "model_failure", + "model rejected the request", + ) + .unwrap(); + let blocked = wait_for_backend_job_worker_cleanup( + &api, + &request.job_id, + &first.attempt.attempt_id, + BackendJobWorkerCleanupState::Failed, + ) + .await; + assert_eq!(blocked.failure_category.as_deref(), Some("model_failure")); + assert_eq!( + blocked.worker_cleanup_failure_category.as_deref(), + Some("backend_job_cleanup_removal_rejected") + ); + + let second = api.retry_backend_job(&request.job_id).unwrap(); + let second_worker = second.attempt.worker.clone().unwrap(); + seed_worker_session_for_cleanup(temp.path(), &second_worker); + assert_ne!(first_worker, second_worker); + let _ = set_worker_retention( + api.clone(), + first_worker.runtime_id.clone(), + first_worker.worker_id.clone(), + false, + ) + .await + .unwrap(); + api.recover_backend_jobs().unwrap(); + wait_for_backend_job_worker_cleanup( + &api, + &request.job_id, + &first.attempt.attempt_id, + BackendJobWorkerCleanupState::Completed, + ) + .await; + assert!(api.runtime.worker(&second_worker).is_ok()); + + api.accept_backend_job_result( + &second_worker, + &BackendJobResultSubmission { + job_id: request.job_id.clone(), + attempt_id: second.attempt.attempt_id.clone(), + input_revision: request.input_revision.clone(), + result: serde_json::json!({"retried": true}), + }, + ) + .unwrap(); + wait_for_backend_job_worker_cleanup( + &api, + &request.job_id, + &second.attempt.attempt_id, + BackendJobWorkerCleanupState::Completed, + ) + .await; + assert_eq!( + api.store + .get_backend_job_attempt( + TEST_WORKSPACE_ID, + &request.job_id, + &first.attempt.attempt_id, + ) + .unwrap() + .unwrap() + .failure_detail + .as_deref(), + Some("model rejected the request") + ); + } + + #[tokio::test] + async fn backend_job_cleanup_restart_before_run_finish_marker_converges() { + let temp = tempfile::tempdir().unwrap(); + let config = test_server_config(temp.path()); + let request = BackendJobRequest { + job_id: "runner-cleanup-pre-marker-restart-1".to_string(), + purpose: "cleanup_pre_marker_restart_contract_check".to_string(), + input_revision: "revision-1".to_string(), + input_ref: "test://runner/cleanup-pre-marker-restart/1".to_string(), + input: serde_json::json!({"value": "immutable"}), + instruction: "Return a structured check result.".to_string(), + profile: "builtin:backend-job".to_string(), + source_worker: None, + notification_target: None, + limits: crate::backend_job::BackendJobLimits::default(), + }; + let store = Arc::new(SqliteWorkspaceStore::open(config.database_path.clone()).unwrap()); + seed_test_registered_workspace(store.as_ref(), &config) + .await + .unwrap(); + let execution = Arc::new(DeterministicExecutionBackend::default()); + let embedded = EmbeddedWorkerRuntime::new_fs_store_with_execution_backend( + TEST_WORKSPACE_ID, + config.embedded_runtime_store_root.clone(), + execution.clone(), + ) + .unwrap(); + let binding = test_create_binding(); + let runtime = embedded.subscription_runtime(); + let runtime_worker_ref = worker_runtime::identity::WorkerRef::new(binding.worker_id); + let detail = runtime + .create_worker_scoped( + &worker_runtime::RuntimeWorkspaceScope::new(TEST_WORKSPACE_ID, "embedded-backend"), + worker_runtime::catalog::CreateWorkerRequest { + worker_id: binding.worker_id, + create_fingerprint: binding.create_fingerprint, + profile: ProfileSelector::Builtin(request.profile.clone()), + display_name: Some("job:cleanup_pre_marker_restart_contract_check".to_string()), + config_bundle: None, + profile_source: worker_runtime::catalog::ProfileSourceArchiveSource::Embedded { + archive: crate::profile_settings::builtin_profile_source_archive( + &ProfileSelector::Builtin(request.profile.clone()), + ) + .unwrap(), + }, + initial_input: None, + workdir_attachment_requests: Vec::new(), + workdir_attachments: Vec::new(), + worker_observation_enabled: false, + worker_observation_grants: Vec::new(), + workspace_api: Some(test_worker_workspace_api(EMBEDDED_WORKER_RUNTIME_ID)), + memory_settings: Some(test_worker_memory_settings()), + subjektiv_attached: false, + }, + ) + .unwrap(); + let worker = + RuntimeWorkerRef::new(EMBEDDED_WORKER_RUNTIME_ID, detail.worker_id.to_string()); + runtime + .observe_worker_event( + &runtime_worker_ref, + protocol::Event::WorkerState { + snapshot: protocol::WorkerStateSnapshot { + last_command_id: 0, + last_finished_submission_request_id: None, + state: protocol::WorkerState::Busy(protocol::WorkerBusyState::Run( + protocol::WorkerRunState::Running, + )), + }, + }, + ) + .unwrap(); + let now = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true); + let reservation = store + .reserve_backend_job(TEST_WORKSPACE_ID, &request, &now) + .unwrap(); + let (_, claimed) = store + .claim_backend_job_attempt_dispatch( + TEST_WORKSPACE_ID, + &request.job_id, + &reservation.attempt.attempt_id, + &now, + ) + .unwrap(); + assert!(claimed); + let runtime_run_id = crate::backend_job::runtime_run_id(&reservation.attempt.attempt_id); + store + .upsert_worker_registry(&WorkerRegistryRecord { + workspace_id: TEST_WORKSPACE_ID.to_string(), + worker: worker.clone(), + display_name: "job:cleanup_pre_marker_restart_contract_check".to_string(), + profile: Some(request.profile.clone()), + retention_state: "normal".to_string(), + transcript_ref: None, + session_ref: None, + summary_ref: None, + diagnostics_ref: None, + created_at: now.clone(), + updated_at: now.clone(), + }) + .unwrap(); + store + .bind_backend_job_attempt_worker( + TEST_WORKSPACE_ID, + &request.job_id, + &reservation.attempt.attempt_id, + &worker, + Some(&runtime_run_id), + &now, + ) + .unwrap(); + seed_worker_session_for_cleanup(temp.path(), &worker); + let before_restart = runtime.worker_detail(&runtime_worker_ref).unwrap(); + assert!(!before_restart.execution_reconstructed); + assert!(before_restart.worker_state.is_some_and(|snapshot| { + snapshot.last_command_id == 0 + && snapshot.last_finished_submission_request_id.is_none() + && snapshot.state + == protocol::WorkerState::Busy(protocol::WorkerBusyState::Run( + protocol::WorkerRunState::Running, + )) + })); + store + .accept_backend_job_result( + TEST_WORKSPACE_ID, + &worker, + &BackendJobResultSubmission { + job_id: request.job_id.clone(), + attempt_id: reservation.attempt.attempt_id.clone(), + input_revision: request.input_revision.clone(), + result: serde_json::json!({"persisted": true}), + }, + &now, + ) + .unwrap(); + assert_eq!( + store + .get_backend_job_attempt( + TEST_WORKSPACE_ID, + &request.job_id, + &reservation.attempt.attempt_id, + ) + .unwrap() + .unwrap() + .worker_cleanup_state, + Some(BackendJobWorkerCleanupState::Pending) + ); + let attempt_id = reservation.attempt.attempt_id; + drop(runtime); + drop(embedded); + drop(store); + drop(execution); + + let restarted_store = SqliteWorkspaceStore::open(config.database_path.clone()).unwrap(); + let restarted_execution = Arc::new(DeterministicExecutionBackend::default()); + restarted_execution.accept_restores(); + let restarted = WorkspaceApi::new_with_execution_backend( + config, + Arc::new(restarted_store), + restarted_execution, + ) + .await + .unwrap(); + let cleaned = wait_for_backend_job_worker_cleanup( + &restarted, + &request.job_id, + &attempt_id, + BackendJobWorkerCleanupState::Completed, + ) + .await; + assert_eq!(cleaned.result, Some(serde_json::json!({"persisted": true}))); + assert!(matches!( + restarted.runtime.worker(&worker), + Err(RuntimeRegistryError::UnknownWorker { .. }) + )); + } + #[tokio::test] async fn backend_job_fast_result_is_accepted_after_prebound_tracked_run() { let temp = tempfile::tempdir().unwrap(); @@ -42445,9 +43429,11 @@ mod tests { let hook_api = api.clone(); let hook_request = request.clone(); let hook_observed = acceptance_observed.clone(); + let hook_root = temp.path().to_path_buf(); execution.before_input_returns(move |worker| { let source = RuntimeWorkerRef::new(EMBEDDED_WORKER_RUNTIME_ID, worker.worker_id.to_string()); + seed_worker_session_for_cleanup(&hook_root, &source); let accepted = hook_api .accept_backend_job_result( &source, @@ -42471,6 +43457,113 @@ mod tests { dispatched.attempt.runtime_run_id.as_deref(), Some(expected_run_id.as_str()) ); + let worker = dispatched.attempt.worker.clone().unwrap(); + wait_for_backend_job_worker_cleanup( + &api, + &request.job_id, + &dispatched.attempt.attempt_id, + BackendJobWorkerCleanupState::Completed, + ) + .await; + assert!(matches!( + api.runtime.worker(&worker), + Err(RuntimeRegistryError::UnknownWorker { .. }) + )); + } + + #[tokio::test] + async fn backend_job_result_http_response_precedes_cleanup_until_tracked_run_finishes() { + let temp = tempfile::tempdir().unwrap(); + let (api, execution) = test_api_with_recording_backend(temp.path()).await; + let request = BackendJobRequest { + job_id: "runner-result-response-fence-1".to_string(), + purpose: "result_response_fence_check".to_string(), + input_revision: "revision-1".to_string(), + input_ref: "test://runner/result-response-fence/1".to_string(), + input: serde_json::json!({"value": "immutable"}), + instruction: "Return a structured check result.".to_string(), + profile: "builtin:backend-job".to_string(), + source_worker: None, + notification_target: None, + limits: crate::backend_job::BackendJobLimits::default(), + }; + let dispatched = api.dispatch_backend_job(&request).unwrap(); + let worker = dispatched.attempt.worker.clone().unwrap(); + let runtime_run_id = dispatched.attempt.runtime_run_id.clone().unwrap(); + seed_worker_session_for_cleanup(temp.path(), &worker); + execution.publish_worker_state( + &worker, + protocol::WorkerStateSnapshot { + last_command_id: 0, + last_finished_submission_request_id: None, + state: protocol::WorkerState::Busy(protocol::WorkerBusyState::Run( + protocol::WorkerRunState::Running, + )), + }, + ); + assert!( + api.runtime + .worker(&worker) + .unwrap() + .worker_state + .is_some_and(|snapshot| snapshot.state + == protocol::WorkerState::Busy(protocol::WorkerBusyState::Run( + protocol::WorkerRunState::Running + ))) + ); + + let Json(acceptance) = scoped_submit_backend_job_result( + State(api.clone()), + AxumPath(ScopedWorkspacePath { + workspace_id: TEST_WORKSPACE_ID.to_string(), + }), + ticket_check_headers(&worker), + Json(SubmitBackendJobResultRequest { + result: serde_json::json!({"valid": true}), + }), + ) + .await + .unwrap(); + assert_eq!(acceptance.job.state, BackendJobState::Completed); + let still_running = api.runtime.worker(&worker).unwrap(); + assert!(still_running.worker_state.is_some_and(|snapshot| { + snapshot.state + == protocol::WorkerState::Busy(protocol::WorkerBusyState::Run( + protocol::WorkerRunState::Running, + )) + })); + assert_eq!( + api.store + .get_backend_job_attempt( + TEST_WORKSPACE_ID, + &request.job_id, + &dispatched.attempt.attempt_id, + ) + .unwrap() + .unwrap() + .worker_cleanup_state, + Some(BackendJobWorkerCleanupState::Executing) + ); + + execution.publish_worker_state( + &worker, + protocol::WorkerStateSnapshot { + last_command_id: 0, + last_finished_submission_request_id: Some(runtime_run_id), + state: protocol::WorkerState::Idle, + }, + ); + wait_for_backend_job_worker_cleanup( + &api, + &request.job_id, + &dispatched.attempt.attempt_id, + BackendJobWorkerCleanupState::Completed, + ) + .await; + assert!(matches!( + api.runtime.worker(&worker), + Err(RuntimeRegistryError::UnknownWorker { .. }) + )); } #[tokio::test] @@ -42628,6 +43721,12 @@ mod tests { async fn backend_job_input_error_is_durable_unknown_and_not_automatically_retried() { let temp = tempfile::tempdir().unwrap(); let (api, execution) = test_api_with_recording_backend(temp.path()).await; + let cleanup_root = temp.path().to_path_buf(); + execution.before_input_returns(move |worker| { + let worker = + RuntimeWorkerRef::new(EMBEDDED_WORKER_RUNTIME_ID, worker.worker_id.to_string()); + seed_worker_session_for_cleanup(&cleanup_root, &worker); + }); execution.reject_inputs("model transport unavailable"); let request = BackendJobRequest { job_id: "runner-input-failure-1".to_string(), @@ -42657,6 +43756,18 @@ mod tests { failed.failure_category.as_deref(), Some("input_outcome_unknown") ); + let worker = failed.worker.clone().unwrap(); + wait_for_backend_job_worker_cleanup( + &api, + &request.job_id, + &failed.attempt_id, + BackendJobWorkerCleanupState::Completed, + ) + .await; + assert!(matches!( + api.runtime.worker(&worker), + Err(RuntimeRegistryError::UnknownWorker { .. }) + )); assert!( api.retry_backend_job(&request.job_id).is_err(), "an ambiguous input outcome must not be retried automatically" @@ -42877,6 +43988,7 @@ mod tests { }; let dispatched = api.dispatch_backend_job(&request).unwrap(); let worker = dispatched.attempt.worker.clone().unwrap(); + seed_worker_session_for_cleanup(temp.path(), &worker); tokio::time::sleep(std::time::Duration::from_millis(1_200)).await; let attempt = api .store @@ -42889,6 +44001,17 @@ mod tests { .unwrap(); assert_eq!(attempt.state, BackendJobAttemptState::Failed); assert_eq!(attempt.failure_category.as_deref(), Some("timeout")); + wait_for_backend_job_worker_cleanup( + &api, + &request.job_id, + &dispatched.attempt.attempt_id, + BackendJobWorkerCleanupState::Completed, + ) + .await; + assert!(matches!( + api.runtime.worker(&worker), + Err(RuntimeRegistryError::UnknownWorker { .. }) + )); assert!( api.accept_backend_job_result( &worker, @@ -45229,6 +46352,7 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }), ) @@ -51568,7 +52692,8 @@ mod tests { "alias": "workdir", "working_directory_id": working_directory_id, "relative_cwd": "../escape" - }] + }], + "feature_connections": {} })), StatusCode::BAD_REQUEST, ) @@ -52268,7 +53393,8 @@ mod tests { serde_json::json!({ "runtime_id": "embedded-worker-runtime", "display_name": "", - "initial_submit": [] + "initial_submit": [], + "feature_connections": {} }), ) .await; @@ -52387,10 +53513,10 @@ mod tests { ticket_assignment: None, initial_submit: Vec::new(), workdir_attachments: Vec::new(), + feature_connections: Default::default(), control_operation_id: None, }, None, - false, ) .await .unwrap(); @@ -54807,7 +55933,7 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3); assert!(subjektiv_scope_from_singleton_key("subjektiv:").is_err()); assert!(subjektiv_scope_from_singleton_key("subjektiv: subject-42").is_err()); assert_eq!( - subjektiv_singleton_key("subject-42").unwrap(), + crate::subjektiv::subject_worker_singleton_key("subject-42").unwrap(), "subjektiv:subject-42" ); } diff --git a/crates/workspace-server/src/store.rs b/crates/workspace-server/src/store.rs index 30ecf1aa7..ecfebdec8 100644 --- a/crates/workspace-server/src/store.rs +++ b/crates/workspace-server/src/store.rs @@ -21,13 +21,14 @@ use crate::backend_job::{ BackendJobAttemptRecord, BackendJobAttemptState, BackendJobDeliveryRecord, BackendJobDeliveryState, BackendJobRecord, BackendJobRequest, BackendJobReservation, BackendJobResultAcceptance, BackendJobResultSubmission, BackendJobState, - BackendJobWorkerBinding, attempt_id, bounded_failure_detail, result_digest, + BackendJobWorkerBinding, BackendJobWorkerCleanupState, attempt_id, bounded_failure_detail, + result_digest, }; use crate::workspace_deletion::WorkspaceDeletionStore; use crate::{Error, Result}; const OLDEST_SCHEMA_VERSION: i64 = 50; -const LATEST_SCHEMA_VERSION: i64 = 76; +const LATEST_SCHEMA_VERSION: i64 = 77; const SCHEMA_BASELINE_NAME: &str = "workspace schema baseline"; const WORKSPACE_RUNTIME_BINDINGS_MIGRATION_NAME: &str = "workspace runtime bindings"; const RUNTIME_BINDING_AUDIT_MIGRATION_NAME: &str = "workspace Runtime binding revision and audit"; @@ -74,6 +75,8 @@ const RUNTIME_SCOPED_WORKER_IDENTITY_MIGRATION_NAME: &str = "Runtime-scoped durable Worker catalog identity"; const ARCHIVE_OBSERVE_GRANTS_MIGRATION_NAME: &str = "preserve explicit observe grants for committed Worker Session archives"; +const BACKEND_JOB_WORKER_CLEANUP_MIGRATION_NAME: &str = + "durable terminal Backend Job Worker cleanup"; const TICKET_SCHEMA_VERSION_WITH_TARGETS: i64 = 7; const TICKET_SCHEMA_BASELINE_NAME: &str = "ticket schema baseline"; const WORKER_REGISTRY_PROJECTION_SCHEMA: &str = r#" @@ -251,6 +254,11 @@ const MIGRATIONS: &[Migration] = &[ name: ARCHIVE_OBSERVE_GRANTS_MIGRATION_NAME, apply: migrate_archive_observe_grants_v75_to_v76, }, + Migration { + version: 77, + name: BACKEND_JOB_WORKER_CLEANUP_MIGRATION_NAME, + apply: migrate_backend_job_worker_cleanup_v76_to_v77, + }, ]; #[derive(Clone, Copy)] @@ -1592,6 +1600,34 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore { failure_detail: &str, now: &str, ) -> Result; + fn recover_backend_job_worker_cleanups( + &self, + workspace_id: &str, + now: &str, + limit: usize, + ) -> Result>; + fn list_backend_job_worker_cleanups( + &self, + workspace_id: &str, + limit: usize, + ) -> Result>; + fn claim_backend_job_worker_cleanup( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + now: &str, + ) -> Result<(BackendJobAttemptRecord, bool)>; + fn finish_backend_job_worker_cleanup( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + state: BackendJobWorkerCleanupState, + failure_category: Option<&str>, + failure_detail: Option<&str>, + now: &str, + ) -> Result; fn reserve_backend_job_delivery( &self, workspace_id: &str, @@ -6648,7 +6684,7 @@ impl ControlPlaneStore for SqliteWorkspaceStore { )); } let attempt_changed = tx.execute( - "UPDATE backend_job_attempts SET state = 'completed', result_json = ?4, result_digest = ?5, failure_category = NULL, failure_detail = NULL, completed_at = ?6, updated_at = ?6 WHERE workspace_id = ?1 AND job_id = ?2 AND attempt_id = ?3 AND state = 'dispatched'", + "UPDATE backend_job_attempts SET state = 'completed', result_json = ?4, result_digest = ?5, failure_category = NULL, failure_detail = NULL, worker_cleanup_state = 'pending', worker_cleanup_failure_category = NULL, worker_cleanup_failure_detail = NULL, worker_cleanup_updated_at = ?6, worker_cleanup_completed_at = NULL, completed_at = ?6, updated_at = ?6 WHERE workspace_id = ?1 AND job_id = ?2 AND attempt_id = ?3 AND state = 'dispatched'", params![workspace_id, submission.job_id, submission.attempt_id, result_json, digest, now], )?; let job_changed = tx.execute( @@ -6707,7 +6743,7 @@ impl ControlPlaneStore for SqliteWorkspaceStore { return Ok(attempt); } tx.execute( - "UPDATE backend_job_attempts SET state = ?4, failure_category = ?5, failure_detail = ?6, completed_at = ?7, updated_at = ?7 WHERE workspace_id = ?1 AND job_id = ?2 AND attempt_id = ?3 AND state IN ('reserved', 'dispatching', 'dispatched')", + "UPDATE backend_job_attempts SET state = ?4, failure_category = ?5, failure_detail = ?6, worker_cleanup_state = CASE WHEN worker_id IS NULL THEN 'completed' ELSE 'pending' END, worker_cleanup_failure_category = NULL, worker_cleanup_failure_detail = NULL, worker_cleanup_updated_at = ?7, worker_cleanup_completed_at = CASE WHEN worker_id IS NULL THEN ?7 ELSE NULL END, completed_at = ?7, updated_at = ?7 WHERE workspace_id = ?1 AND job_id = ?2 AND attempt_id = ?3 AND state IN ('reserved', 'dispatching', 'dispatched')", params![workspace_id, job_id, attempt_id, state.as_str(), failure_category, detail, now], )?; if attempt.attempt == job.current_attempt && job.state == BackendJobState::Pending { @@ -6728,6 +6764,154 @@ impl ControlPlaneStore for SqliteWorkspaceStore { }) } + fn recover_backend_job_worker_cleanups( + &self, + workspace_id: &str, + now: &str, + limit: usize, + ) -> Result> { + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + tx.execute( + "UPDATE backend_job_attempts SET worker_cleanup_state = 'pending', worker_cleanup_failure_category = 'cleanup_interrupted', worker_cleanup_failure_detail = 'Backend restart interrupted Worker cleanup; retrying canonical removal', worker_cleanup_updated_at = ?2 WHERE workspace_id = ?1 AND worker_cleanup_state = 'executing'", + params![workspace_id, now], + )?; + let mut statement = tx.prepare( + "SELECT job_id, attempt_id FROM backend_job_attempts WHERE workspace_id = ?1 AND state IN ('completed', 'failed', 'unknown') AND runtime_id IS NOT NULL AND worker_cleanup_state IN ('pending', 'failed') ORDER BY worker_cleanup_updated_at, created_at LIMIT ?2", + )?; + let identities = statement + .query_map(params![workspace_id, limit as i64], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + })? + .collect::>>()?; + drop(statement); + let attempts = identities + .into_iter() + .map(|(job_id, attempt_id)| { + read_backend_job_attempt(&tx, workspace_id, &job_id, &attempt_id)?.ok_or_else( + || Error::Store("recoverable Backend Job Worker cleanup disappeared".to_string()), + ) + }) + .collect::>>()?; + tx.commit()?; + Ok(attempts) + }) + } + + fn list_backend_job_worker_cleanups( + &self, + workspace_id: &str, + limit: usize, + ) -> Result> { + self.with_conn(|conn| { + let mut statement = conn.prepare( + "SELECT job_id, attempt_id FROM backend_job_attempts WHERE workspace_id = ?1 AND state IN ('completed', 'failed', 'unknown') AND runtime_id IS NOT NULL AND worker_cleanup_state IN ('pending', 'failed') ORDER BY worker_cleanup_updated_at, created_at LIMIT ?2", + )?; + let identities = statement + .query_map(params![workspace_id, limit as i64], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + })? + .collect::>>()?; + identities + .into_iter() + .map(|(job_id, attempt_id)| { + read_backend_job_attempt(conn, workspace_id, &job_id, &attempt_id)?.ok_or_else( + || Error::Store("recoverable Backend Job Worker cleanup disappeared".to_string()), + ) + }) + .collect() + }) + } + + fn claim_backend_job_worker_cleanup( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + now: &str, + ) -> Result<(BackendJobAttemptRecord, bool)> { + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let attempt = read_backend_job_attempt(&tx, workspace_id, job_id, attempt_id)? + .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job attempt `{attempt_id}`")))?; + if !attempt.state.terminal() || attempt.worker.is_none() { + return Err(Error::InvalidInput( + "Backend Job Worker cleanup requires a terminal bound attempt".to_string(), + )); + } + if !matches!( + attempt.worker_cleanup_state, + Some(BackendJobWorkerCleanupState::Pending | BackendJobWorkerCleanupState::Failed) + ) { + tx.commit()?; + return Ok((attempt, false)); + } + let changed = tx.execute( + "UPDATE backend_job_attempts SET worker_cleanup_state = 'executing', worker_cleanup_failure_category = NULL, worker_cleanup_failure_detail = NULL, worker_cleanup_updated_at = ?4 WHERE workspace_id = ?1 AND job_id = ?2 AND attempt_id = ?3 AND worker_cleanup_state IN ('pending', 'failed')", + params![workspace_id, job_id, attempt_id, now], + )?; + if changed != 1 { + return Err(Error::Store( + "Backend Job Worker cleanup claim compare-and-set failed".to_string(), + )); + } + let attempt = read_backend_job_attempt(&tx, workspace_id, job_id, attempt_id)? + .ok_or_else(|| Error::Store("claimed Backend Job Worker cleanup disappeared".to_string()))?; + tx.commit()?; + Ok((attempt, true)) + }) + } + + fn finish_backend_job_worker_cleanup( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + state: BackendJobWorkerCleanupState, + failure_category: Option<&str>, + failure_detail: Option<&str>, + now: &str, + ) -> Result { + if !matches!( + state, + BackendJobWorkerCleanupState::Completed | BackendJobWorkerCleanupState::Failed + ) { + return Err(Error::InvalidInput( + "Backend Job Worker cleanup completion requires completed or failed state" + .to_string(), + )); + } + let failure_detail = failure_detail.map(bounded_failure_detail); + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let current = read_backend_job_attempt(&tx, workspace_id, job_id, attempt_id)? + .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job attempt `{attempt_id}`")))?; + if current.worker_cleanup_state == Some(state) { + tx.commit()?; + return Ok(current); + } + if current.worker_cleanup_state != Some(BackendJobWorkerCleanupState::Executing) { + return Err(Error::InvalidInput( + "Backend Job Worker cleanup is not executing".to_string(), + )); + } + let completed_at = (state == BackendJobWorkerCleanupState::Completed).then_some(now); + let changed = tx.execute( + "UPDATE backend_job_attempts SET worker_cleanup_state = ?4, worker_cleanup_failure_category = ?5, worker_cleanup_failure_detail = ?6, worker_cleanup_updated_at = ?7, worker_cleanup_completed_at = ?8 WHERE workspace_id = ?1 AND job_id = ?2 AND attempt_id = ?3 AND worker_cleanup_state = 'executing'", + params![workspace_id, job_id, attempt_id, state.as_str(), failure_category, failure_detail, now, completed_at], + )?; + if changed != 1 { + return Err(Error::Store( + "Backend Job Worker cleanup completion compare-and-set failed".to_string(), + )); + } + let attempt = read_backend_job_attempt(&tx, workspace_id, job_id, attempt_id)? + .ok_or_else(|| Error::Store("finished Backend Job Worker cleanup disappeared".to_string()))?; + tx.commit()?; + Ok(attempt) + }) + } + fn reserve_backend_job_delivery( &self, workspace_id: &str, @@ -10457,14 +10641,16 @@ fn read_backend_job_attempt( attempt_id: &str, ) -> Result> { let row = conn.query_row( - "SELECT attempt, input_revision, state, runtime_id, worker_id, runtime_run_id, dispatched_at, deadline_at, result_json, result_digest, failure_category, failure_detail, created_at, updated_at, completed_at FROM backend_job_attempts WHERE workspace_id = ?1 AND job_id = ?2 AND attempt_id = ?3", + "SELECT attempt, input_revision, state, runtime_id, worker_id, runtime_run_id, dispatched_at, deadline_at, result_json, result_digest, failure_category, failure_detail, worker_cleanup_state, worker_cleanup_failure_category, worker_cleanup_failure_detail, worker_cleanup_updated_at, worker_cleanup_completed_at, created_at, updated_at, completed_at FROM backend_job_attempts WHERE workspace_id = ?1 AND job_id = ?2 AND attempt_id = ?3", params![workspace_id, job_id, attempt_id], |row| Ok(( row.get::<_, i64>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, row.get::<_, Option>(3)?, row.get::<_, Option>(4)?, row.get::<_, Option>(5)?, row.get::<_, Option>(6)?, row.get::<_, String>(7)?, row.get::<_, Option>(8)?, row.get::<_, Option>(9)?, row.get::<_, Option>(10)?, row.get::<_, Option>(11)?, - row.get::<_, String>(12)?, row.get::<_, String>(13)?, row.get::<_, Option>(14)?, + row.get::<_, Option>(12)?, row.get::<_, Option>(13)?, row.get::<_, Option>(14)?, + row.get::<_, Option>(15)?, row.get::<_, Option>(16)?, row.get::<_, String>(17)?, + row.get::<_, String>(18)?, row.get::<_, Option>(19)?, )), ).optional()?; let Some(( @@ -10480,6 +10666,11 @@ fn read_backend_job_attempt( result_digest, failure_category, failure_detail, + worker_cleanup_state, + worker_cleanup_failure_category, + worker_cleanup_failure_detail, + worker_cleanup_updated_at, + worker_cleanup_completed_at, created_at, updated_at, completed_at, @@ -10521,6 +10712,14 @@ fn read_backend_job_attempt( result_digest, failure_category, failure_detail, + worker_cleanup_state: worker_cleanup_state + .as_deref() + .map(BackendJobWorkerCleanupState::parse) + .transpose()?, + worker_cleanup_failure_category, + worker_cleanup_failure_detail, + worker_cleanup_updated_at, + worker_cleanup_completed_at, created_at, updated_at, completed_at, @@ -14248,6 +14447,45 @@ fn migrate_archive_observe_grants_v75_to_v76(conn: &Connection) -> Result<()> { Ok(()) } +fn migrate_backend_job_worker_cleanup_v76_to_v77(conn: &Connection) -> Result<()> { + let current = current_schema_version(conn)?; + if current != 76 { + return Err(Error::Store(format!( + "expected schema version 76 before {BACKEND_JOB_WORKER_CLEANUP_MIGRATION_NAME} migration, found {current}" + ))); + } + let tx = rusqlite::Transaction::new_unchecked(conn, TransactionBehavior::Exclusive)?; + tx.execute_batch( + r#" + ALTER TABLE backend_job_attempts ADD COLUMN worker_cleanup_state TEXT + CHECK (worker_cleanup_state IN ('pending', 'executing', 'completed', 'failed')); + ALTER TABLE backend_job_attempts ADD COLUMN worker_cleanup_failure_category TEXT; + ALTER TABLE backend_job_attempts ADD COLUMN worker_cleanup_failure_detail TEXT; + ALTER TABLE backend_job_attempts ADD COLUMN worker_cleanup_updated_at TEXT; + ALTER TABLE backend_job_attempts ADD COLUMN worker_cleanup_completed_at TEXT; + UPDATE backend_job_attempts + SET worker_cleanup_state = CASE + WHEN runtime_id IS NULL THEN 'completed' + ELSE 'pending' + END, + worker_cleanup_updated_at = updated_at, + worker_cleanup_completed_at = CASE + WHEN runtime_id IS NULL THEN updated_at + ELSE NULL + END + WHERE state IN ('completed', 'failed', 'unknown'); + CREATE INDEX backend_job_attempts_worker_cleanup + ON backend_job_attempts(workspace_id, worker_cleanup_state, updated_at); + "#, + )?; + tx.execute( + "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", + params![77_i64, BACKEND_JOB_WORKER_CLEANUP_MIGRATION_NAME], + )?; + tx.commit()?; + Ok(()) +} + fn create_latest_workspace_schema(conn: &Connection) -> Result<()> { conn.execute_batch("DROP TABLE IF EXISTS typed_ticket_targets;")?; conn.execute_batch(include_str!("latest_schema.sql"))?; @@ -16810,6 +17048,10 @@ mod tests { version: 76, name: ARCHIVE_OBSERVE_GRANTS_MIGRATION_NAME.to_string(), }, + WorkspaceSchemaMigrationStep { + version: 77, + name: BACKEND_JOB_WORKER_CLEANUP_MIGRATION_NAME.to_string(), + }, ] ); @@ -16893,6 +17135,10 @@ mod tests { RUNTIME_SCOPED_WORKER_IDENTITY_MIGRATION_NAME.to_string(), ), (76, ARCHIVE_OBSERVE_GRANTS_MIGRATION_NAME.to_string()), + ( + 77, + BACKEND_JOB_WORKER_CLEANUP_MIGRATION_NAME.to_string(), + ), ] ); assert!(!table_exists(conn, "trusted_runtime_records")?); @@ -17160,6 +17406,71 @@ mod tests { ); } + #[test] + fn backend_job_worker_cleanup_migration_queues_legacy_terminal_workers() { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch( + r#" + CREATE TABLE __yoi_schema_migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL + ); + INSERT INTO __yoi_schema_migrations(version, name) + VALUES (76, 'workspace schema baseline'); + CREATE TABLE backend_job_attempts ( + workspace_id TEXT NOT NULL, + job_id TEXT NOT NULL, + attempt_id TEXT NOT NULL, + state TEXT NOT NULL, + runtime_id TEXT, + worker_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + INSERT INTO backend_job_attempts VALUES + ('w', 'terminal-bound', 'a1', 'completed', 'runtime', 'worker', '1', '2'), + ('w', 'terminal-unbound', 'a2', 'failed', NULL, NULL, '1', '3'), + ('w', 'running', 'a3', 'dispatched', 'runtime', 'running-worker', '1', '4'); + "#, + ) + .unwrap(); + + migrate_backend_job_worker_cleanup_v76_to_v77(&conn).unwrap(); + + let states = conn + .prepare( + "SELECT job_id, worker_cleanup_state, worker_cleanup_completed_at FROM backend_job_attempts ORDER BY job_id", + ) + .unwrap() + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, Option>(1)?, + row.get::<_, Option>(2)?, + )) + }) + .unwrap() + .collect::>>() + .unwrap(); + assert_eq!( + states, + vec![ + ("running".to_string(), None, None,), + ( + "terminal-bound".to_string(), + Some("pending".to_string()), + None, + ), + ( + "terminal-unbound".to_string(), + Some("completed".to_string()), + Some("3".to_string()), + ), + ] + ); + assert_eq!(current_schema_version(&conn).unwrap(), 77); + } + #[test] fn migration_resumes_from_a_valid_partially_applied_chain() { let temp = tempfile::tempdir().unwrap(); @@ -17179,7 +17490,7 @@ mod tests { .collect::>(), vec![ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, - 73, 74, 75, 76 + 73, 74, 75, 76, 77 ] ); SqliteWorkspaceStore::migrate_database(&path).unwrap(); @@ -17188,7 +17499,7 @@ mod tests { current_schema_version(&conn).unwrap(), LATEST_SCHEMA_VERSION ); - assert_eq!(workspace_schema_migration_history(&conn).unwrap().len(), 27); + assert_eq!(workspace_schema_migration_history(&conn).unwrap().len(), 28); assert!(column_exists(&conn, "worker_workdir_links", "alias").unwrap()); assert!(column_exists(&conn, "worker_workdir_links", "capabilities").unwrap()); assert!(!column_exists(&conn, "worker_workdir_links", "role").unwrap()); diff --git a/crates/workspace-server/src/subjektiv.rs b/crates/workspace-server/src/subjektiv.rs index 1c66bc387..37fad4588 100644 --- a/crates/workspace-server/src/subjektiv.rs +++ b/crates/workspace-server/src/subjektiv.rs @@ -22,6 +22,7 @@ use crate::feature_storage::{ pub const SUBJEKTIV_SCHEMA_VERSION: u32 = 1; pub const SUBJEKTIV_FEATURE_ID: &str = "subjektiv"; +pub const SUBJECT_WORKER_SINGLETON_PREFIX: &str = "subjektiv:"; pub const MAX_STAGING_ANCHORS: usize = 10; /// Deterministic surface-generation policy. Input estimates use the repository's /// provider-independent UTF-8 byte estimate (`ceil(bytes / 4)`). The material @@ -45,6 +46,8 @@ pub enum SubjektivError { SubjectNotFound(String), #[error("subject `{0}` is retired")] SubjectRetired(String), + #[error("the selected Worker Profile does not enable subjektiv")] + ProfileDisabled, #[error("staging candidate `{0}` was not found for this subject")] CandidateNotFound(String), #[error("staging candidate `{0}` already exists with different content")] @@ -147,6 +150,23 @@ pub struct SubjectPage { pub has_more: bool, } +/// Backend-authorized launch material for one active subject. The singleton key +/// is derived by subjektiv and then acquired through Yoi's generic singleton path. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AuthorizedSubjectWorkerAttachment { + pub singleton_key: String, +} + +pub fn subject_worker_singleton_key(subject_id: &str) -> Result { + validate_label("subject id", subject_id)?; + if subject_id.trim() != subject_id { + return Err(SubjektivError::InvalidRecord( + "subject id must not contain surrounding whitespace".to_string(), + )); + } + Ok(format!("{SUBJECT_WORKER_SINGLETON_PREFIX}{subject_id}")) +} + /// Immutable host-recorded history that attributes one durable Session to the /// subject and Worker that produced it. This is archival provenance only: it is /// deliberately not a current-Worker link or execution ownership record. @@ -1091,6 +1111,28 @@ impl SubjektivStore { }) } + /// Validates an explicit product launch connection against this Workspace's + /// subject store and the already-resolved Profile policy. Subject knowledge + /// alone is insufficient: only an active local record produces host launch material. + pub fn authorize_worker_attachment( + &self, + subject_id: &str, + profile: &manifest::WorkerManifest, + ) -> Result { + if !profile.feature.subjektiv.profile.enabled { + return Err(SubjektivError::ProfileDisabled); + } + let subject = self + .subject(subject_id)? + .ok_or_else(|| SubjektivError::SubjectNotFound(subject_id.to_string()))?; + if subject.state != SubjectState::Active { + return Err(SubjektivError::SubjectRetired(subject_id.to_string())); + } + Ok(AuthorizedSubjectWorkerAttachment { + singleton_key: subject_worker_singleton_key(&subject.id)?, + }) + } + /// Lists the first deterministic bounded page of subjects. Ordering uses /// immutable creation time with the host-issued subject id as a total-order /// tie breaker. diff --git a/openapi/server-api.json b/openapi/server-api.json index d1b74ff09..9fa559d03 100644 --- a/openapi/server-api.json +++ b/openapi/server-api.json @@ -1528,6 +1528,10 @@ "display_name": { "type": "string" }, + "feature_connections": { + "$ref": "#/components/schemas/WorkspaceWorkerFeatureConnectionsRequest", + "description": "Explicit product Feature connections. Backend validation turns these into\ntrusted host attachments; Runtime and Profile inputs cannot self-attest them." + }, "initial_submit": { "default": [], "items": { @@ -1574,7 +1578,8 @@ }, "required": [ "runtime_id", - "display_name" + "display_name", + "feature_connections" ], "type": "object" }, @@ -9428,15 +9433,15 @@ ], "type": "string" }, - "SubjektivSubjectWorkerStartRequest": { + "SubjektivWorkerConnectionRequest": { "additionalProperties": false, "properties": { - "worker": { - "$ref": "#/components/schemas/CreateWorkspaceWorkerRequest" + "subject_id": { + "type": "string" } }, "required": [ - "worker" + "subject_id" ], "type": "object" }, @@ -12069,6 +12074,18 @@ ], "type": "object" }, + "WorkerLaunchFeatureConnectionOptions": { + "additionalProperties": false, + "properties": { + "subjektiv": { + "type": "boolean" + } + }, + "required": [ + "subjektiv" + ], + "type": "object" + }, "WorkerLaunchOptionsResponse": { "additionalProperties": false, "properties": { @@ -12128,6 +12145,9 @@ "description": { "type": "string" }, + "feature_connections": { + "$ref": "#/components/schemas/WorkerLaunchFeatureConnectionOptions" + }, "id": { "type": "string" }, @@ -12138,7 +12158,8 @@ "required": [ "id", "label", - "description" + "description", + "feature_connections" ], "type": "object" }, @@ -12547,6 +12568,13 @@ "minimum": 0, "type": "integer" }, + "last_finished_submission_request_id": { + "description": "Most recent Submit request whose controller Run has returned and whose\npost-Run state transition has been published. This is a teardown fence,\nnot a successful-result claim; cancelled and failed Runs may also finish.", + "type": [ + "string", + "null" + ] + }, "state": { "$ref": "#/components/schemas/WorkerState" } @@ -14706,6 +14734,22 @@ ], "type": "object" }, + "WorkspaceWorkerFeatureConnectionsRequest": { + "additionalProperties": false, + "properties": { + "subjektiv": { + "anyOf": [ + { + "$ref": "#/components/schemas/SubjektivWorkerConnectionRequest" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, "WorkspaceWorkerSubject": { "oneOf": [ { @@ -14750,7 +14794,7 @@ "info": { "title": "Yoi Server API", "version": "0.1.0", - "x-yoi-source-digest": "sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098" + "x-yoi-source-digest": "sha256:f9f0f8918dbcce386e94d5368fa938f02565ca98f9f67f07537bd88ed8c49870" }, "openapi": "3.1.0", "paths": { @@ -29642,143 +29686,6 @@ ] } }, - "/api/w/{workspace_id}/subjektiv/subjects/{subject_id}/worker": { - "post": { - "operationId": "subjektiv_subject_worker_start", - "parameters": [ - { - "explode": false, - "in": "path", - "name": "workspace_id", - "required": true, - "schema": { - "$ref": "#/components/schemas/string" - }, - "style": "simple" - }, - { - "explode": false, - "in": "path", - "name": "subject_id", - "required": true, - "schema": { - "$ref": "#/components/schemas/string" - }, - "style": "simple" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubjektivSubjectWorkerStartRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BrowserCreateWorkerResponse" - } - } - }, - "description": "Successful response" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepositoryApiError" - } - } - }, - "description": "Error response" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepositoryApiError" - } - } - }, - "description": "Error response" - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepositoryApiError" - } - } - }, - "description": "Error response" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepositoryApiError" - } - } - }, - "description": "Error response" - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepositoryApiError" - } - } - }, - "description": "Error response" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepositoryApiError" - } - } - }, - "description": "Error response" - }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepositoryApiError" - } - } - }, - "description": "Error response" - }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepositoryApiError" - } - } - }, - "description": "Error response" - } - }, - "security": [ - { - "bearerAuth": [] - }, - { - "browserSession": [] - } - ] - } - }, "/api/w/{workspace_id}/tickets": { "get": { "operationId": "ticket_list", diff --git a/tools/web-ux/browser-tests/memory_document_fixture_server.ts b/tools/web-ux/browser-tests/memory_document_fixture_server.ts index 0e18e55f7..3cd9cc001 100644 --- a/tools/web-ux/browser-tests/memory_document_fixture_server.ts +++ b/tools/web-ux/browser-tests/memory_document_fixture_server.ts @@ -367,6 +367,38 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => { if (url.pathname === `/api/w/${workspaceId}/working-directories`) { return json({ workspace_id: workspaceId, items: [], diagnostics: [] }); } + if (url.pathname === `/api/w/${workspaceId}/workers/launch-options`) { + return json({ + workspace_id: workspaceId, + runtimes: [{ + runtime_id: "embedded-worker-runtime", + display_name: "Embedded Runtime", + built_in: true, + worker_creation_available: true, + working_directory_required: false, + status: "active", + diagnostics: [], + }], + default_profile: "builtin:companion", + profiles: [ + { + id: "builtin:companion", + label: "Companion", + description: "General Workspace assistance with explicit subject Memory support.", + feature_connections: { subjektiv: true }, + }, + { + id: "builtin:standalone", + label: "Standalone", + description: "Independent Worker without Workspace subject Memory support.", + feature_connections: { subjektiv: false }, + }, + ], + repositories: [], + working_directories: [], + diagnostics: [], + }); + } const subjectsPath = `/api/w/${workspaceId}/subjektiv/subjects`; if (url.pathname === subjectsPath) { diff --git a/tools/web-ux/browser-tests/memory_document_shell_test.ts b/tools/web-ux/browser-tests/memory_document_shell_test.ts index c359bfb6e..7ab79f3a4 100644 --- a/tools/web-ux/browser-tests/memory_document_shell_test.ts +++ b/tools/web-ux/browser-tests/memory_document_shell_test.ts @@ -151,7 +151,7 @@ async function swipeMainToEnd(page: Page): Promise { throw new Error("touch input did not reach the end of the resident Memory surface"); } -Deno.test("production subject Memory shell renders exact states, safe Markdown, provenance, and one scroll owner", async () => { +Deno.test("production subject Memory and Worker launch shells preserve exact scoped behavior", async () => { const build = await new Deno.Command(Deno.execPath(), { args: ["task", "build"], cwd: workspaceRoot, @@ -500,6 +500,49 @@ Deno.test("production subject Memory shell renders exact states, safe Markdown, await page.close(); } await stateContext.close(); + + const launchContext = await browser.newContext({ viewport: { width: 768, height: 900 } }); + const launchPage = await launchContext.newPage(); + await launchPage.goto(`${baseUrl}/w/${workspaceId}/workers/new`); + await launchPage.getByRole("heading", { name: "New Worker", level: 1 }).waitFor(); + const subjectConnection = launchPage.locator( + '[data-worker-feature-connection="subjektiv"]', + ); + await subjectConnection.waitFor(); + const subjectSelect = launchPage.getByRole("combobox", { + name: "Subject Memory connection", + }); + assertEquals(await subjectSelect.locator('option[value="error-subject"]').count(), 0); + assertEquals(await subjectSelect.locator('option[value="paged-subject"]').count(), 0); + await launchPage.getByRole("button", { name: "Load more subjects" }).click(); + await subjectSelect.locator('option[value="paged-subject"]').waitFor({ state: "attached" }); + await subjectSelect.selectOption(subjectId); + assertEquals(await subjectSelect.inputValue(), subjectId); + assertEquals( + await subjectConnection.locator("code", { hasText: subjectId }).count(), + 1, + ); + + const profileSelect = launchPage.getByRole("combobox", { name: "Profile" }); + await profileSelect.selectOption("builtin:standalone"); + assertEquals(await subjectConnection.count(), 0); + await profileSelect.selectOption("builtin:companion"); + await subjectConnection.waitFor(); + assertEquals(await subjectSelect.inputValue(), ""); + + await subjectSelect.selectOption(subjectId); + const createRequestPromise = launchPage.waitForRequest((request) => + request.method() === "POST" && + new URL(request.url()).pathname === `/api/w/${workspaceId}/workers` + ); + await launchPage.getByRole("button", { name: "Start Worker" }).click(); + const createRequest = await createRequestPromise; + const createPayload = createRequest.postDataJSON(); + assertEquals(createPayload.profile, "builtin:companion"); + assertEquals(createPayload.feature_connections, { + subjektiv: { subject_id: subjectId }, + }); + await launchContext.close(); } finally { await browser.close(); } diff --git a/tools/web-ux/scenarios/worker-launch-dark.json b/tools/web-ux/scenarios/worker-launch-dark.json new file mode 100644 index 000000000..c091fbd3f --- /dev/null +++ b/tools/web-ux/scenarios/worker-launch-dark.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "id": "worker-launch-dark", + "title": "Ordinary Worker launch with optional subject connection — dark", + "baseUrl": "http://127.0.0.1:15288", + "colorScheme": "dark", + "reducedMotion": "reduce", + "personas": [{ + "id": "fixture", + "label": "Authorized Workspace member fixture", + "auth": { "kind": "anonymous" } + }], + "viewports": [ + { "label": "desktop", "width": 1440, "height": 900 }, + { "label": "narrow", "width": 768, "height": 900 }, + { "label": "mobile", "width": 390, "height": 844 }, + { "label": "compact", "width": 320, "height": 700 } + ], + "routes": [{ + "id": "representative", + "label": "Worker launch", + "path": "/w/memory-review/workers/new", + "goal": "Start an ordinary Worker with a compatible Profile and optionally connect an authorized existing subject.", + "dataState": "Compatible and incompatible Profiles with active, retired, long-identity, and paginated subjects.", + "ready": { "kind": "selector", "selector": "form.worker-launch-form" }, + "capturePoints": [{ "id": "initial", "label": "Worker launch form" }] + }], + "processes": [{ + "id": "worker-launch-fixture", + "command": "deno", + "cwd": "..", + "args": [ + "run", + "--allow-net", + "--allow-read", + "browser-tests/memory_document_fixture_server.ts", + "15288", + "../../web/workspace/build" + ], + "readyUrl": "http://127.0.0.1:15288/health" + }] +} diff --git a/tools/web-ux/scenarios/worker-launch-light.json b/tools/web-ux/scenarios/worker-launch-light.json new file mode 100644 index 000000000..25528519c --- /dev/null +++ b/tools/web-ux/scenarios/worker-launch-light.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "id": "worker-launch-light", + "title": "Ordinary Worker launch with optional subject connection — light", + "baseUrl": "http://127.0.0.1:15287", + "colorScheme": "light", + "reducedMotion": "reduce", + "personas": [{ + "id": "fixture", + "label": "Authorized Workspace member fixture", + "auth": { "kind": "anonymous" } + }], + "viewports": [ + { "label": "desktop", "width": 1440, "height": 900 }, + { "label": "narrow", "width": 768, "height": 900 }, + { "label": "mobile", "width": 390, "height": 844 }, + { "label": "compact", "width": 320, "height": 700 } + ], + "routes": [{ + "id": "representative", + "label": "Worker launch", + "path": "/w/memory-review/workers/new", + "goal": "Start an ordinary Worker with a compatible Profile and optionally connect an authorized existing subject.", + "dataState": "Compatible and incompatible Profiles with active, retired, long-identity, and paginated subjects.", + "ready": { "kind": "selector", "selector": "form.worker-launch-form" }, + "capturePoints": [{ "id": "initial", "label": "Worker launch form" }] + }], + "processes": [{ + "id": "worker-launch-fixture", + "command": "deno", + "cwd": "..", + "args": [ + "run", + "--allow-net", + "--allow-read", + "browser-tests/memory_document_fixture_server.ts", + "15287", + "../../web/workspace/build" + ], + "readyUrl": "http://127.0.0.1:15287/health" + }] +} diff --git a/web/workspace/src/lib/generated/companion-api.ts b/web/workspace/src/lib/generated/companion-api.ts index e791c828a..ddba41954 100644 --- a/web/workspace/src/lib/generated/companion-api.ts +++ b/web/workspace/src/lib/generated/companion-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-companion-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098) +// Input contract: 0.1.0 (sha256:f9f0f8918dbcce386e94d5368fa938f02565ca98f9f67f07537bd88ed8c49870) // Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/companion-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check diff --git a/web/workspace/src/lib/generated/protocol-validator.ts b/web/workspace/src/lib/generated/protocol-validator.ts index 0d86b0914..75e2fdc48 100644 --- a/web/workspace/src/lib/generated/protocol-validator.ts +++ b/web/workspace/src/lib/generated/protocol-validator.ts @@ -21,7 +21,7 @@ const MAX_CORRELATION_ID_BYTES = 128; const MAX_RESOURCE_ID_BYTES = 256; const MAX_WORKER_IDS_PER_SELECTOR = 256; const MAX_REJECTION_MESSAGE_BYTES = 1024; -const subscriptionFrameSchema: unknown = {"$defs":{"Alert":{"additionalProperties":false,"description":"User-facing alert emitted from the Worker layer.\n\nThis is a separate channel from `tracing` (developer logs): entries\nhere are assembled explicitly by the Worker when a condition should be\nsurfaced to the person driving the client. Keep messages short and\nhuman-readable.","properties":{"level":{"$ref":"#/$defs/AlertLevel"},"message":{"type":"string"},"source":{"$ref":"#/$defs/AlertSource"},"timestamp_ms":{"description":"Milliseconds since the Unix epoch.","format":"int64","maximum":9007199254740991.0,"minimum":-9007199254740991.0,"type":"integer"}},"required":["level","source","message","timestamp_ms"],"type":"object"},"AlertLevel":{"enum":["warn","error"],"type":"string"},"AlertSource":{"enum":["worker","engine","compactor","agents_md"],"type":"string"},"CommandEvent":{"oneOf":[{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"kind":{"const":"started","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_call_id":{"type":["string","null"]}},"required":["kind","command_id","observed_at_ms"],"type":"object"},{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"content":{"type":"string"},"end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"kind":{"const":"output","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"start_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"stream":{"$ref":"#/$defs/CommandStream"}},"required":["kind","command_id","stream","start_offset","end_offset","content","observed_at_ms"],"type":"object"},{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"exit_code":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":["integer","null"]},"kind":{"const":"terminal","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"status":{"$ref":"#/$defs/CommandStatus"},"stderr_end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"stdout_end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","command_id","status","stdout_end_offset","stderr_end_offset","observed_at_ms"],"type":"object"}]},"CommandSnapshot":{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"exit_code":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":["integer","null"]},"last_output_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"status":{"$ref":"#/$defs/CommandStatus"},"stderr":{"$ref":"#/$defs/CommandStreamSlice"},"stdout":{"$ref":"#/$defs/CommandStreamSlice"},"tool_call_id":{"type":["string","null"]}},"required":["command_id","status","started_at_ms","observed_at_ms","stdout","stderr"],"type":"object"},"CommandStatus":{"description":"Live provider-owned command status. These values are operational Console\nstate only and are never appended to Worker history.","enum":["running","completed","failed","timed_out","cancelled"],"type":"string"},"CommandStream":{"enum":["stdout","stderr"],"type":"string"},"CommandStreamSlice":{"additionalProperties":false,"properties":{"content":{"type":"string"},"end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"start_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"truncated":{"type":"boolean"}},"required":["start_offset","end_offset","content","truncated"],"type":"object"},"CompactionLifecycle":{"additionalProperties":false,"description":"Stable parent-owned lifecycle for one compaction run.","properties":{"compaction_id":{"type":"string"},"ended_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"error":{"type":["string","null"]},"internal_worker":{"anyOf":[{"$ref":"#/$defs/InternalWorkerRef"},{"type":"null"}]},"new_segment_id":{"type":["string","null"]},"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"schema_version":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"started_at_ms":{"description":"Milliseconds since the Unix epoch.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"state":{"$ref":"#/$defs/CompactionLifecycleState"},"summary":{"type":["string","null"]}},"required":["schema_version","compaction_id","revision","state","started_at_ms"],"type":"object"},"CompactionLifecycleState":{"enum":["running","done","failed","interrupted"],"type":"string"},"CompactionPhase":{"enum":["preparing","summarizing","committing"],"type":"string"},"CompactionTrigger":{"enum":["manual","pre_run","request_threshold"],"type":"string"},"CompletionEntry":{"additionalProperties":false,"description":"One completion candidate for a prefix query.\n\n`value` is a path (file kind).","properties":{"is_dir":{"default":false,"type":"boolean"},"value":{"type":"string"}},"required":["value","is_dir"],"type":"object"},"CompletionKind":{"description":"Kind of completion requested by `Method::ListCompletions`.\n\nMirrors the completion prefix sigil: `@` → `File`.","enum":["file"],"type":"string"},"ContextTokenSource":{"description":"Provenance of the current-context token count in a [`Greeting`].","oneOf":[{"const":"measured","description":"Provider-reported input occupancy from the most recent LLM request.","type":"string"},{"const":"estimated","description":"Current history projected from the Worker's token accounting records.","type":"string"}]},"ContextUsage":{"additionalProperties":false,"description":"Current prompt-context occupancy, distinct from cumulative run traffic.","properties":{"source":{"$ref":"#/$defs/ContextTokenSource"},"tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["tokens","source"],"type":"object"},"ErrorCode":{"enum":["already_running","not_running","not_paused","provider_error","tool_error","invalid_request","internal"],"type":"string"},"Event":{"oneOf":[{"additionalProperties":false,"description":"Durable Submit acceptance. A `Queued` receipt follows the durable FIFO\ncheckpoint. A `Started` receipt follows the pending activation checkpoint;\nthe corresponding [`Event::UserMessage`] marks the later history commit.\nRepeating the same request id and exact payload returns the same receipt\nwithout appending or activating twice.","properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"$ref":"#/$defs/SubmissionDisposition"},"submission_id":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","submission_id","disposition"],"type":"object"},"event":{"const":"submission_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","message"],"type":"object"},"event":{"const":"submission_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Durable notification acceptance. The request identity is persisted with\nthe notification receipt before this event is emitted. Repeating the\nsame request id and exact payload returns the same acknowledgement\nwithout appending the notification twice.","properties":{"data":{"additionalProperties":false,"properties":{"notification_request_id":{"type":"string"}},"required":["notification_request_id"],"type":"object"},"event":{"const":"notification_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated notification rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"},"event":{"const":"notification_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Revisioned FIFO replacement following enqueue, activation, cancel, or clear.","properties":{"data":{"additionalProperties":false,"properties":{"pending":{"$ref":"#/$defs/PendingSubmissionsSnapshot"}},"required":["pending"],"type":"object"},"event":{"const":"pending_submissions_changed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A user input message persisted as `LogEntry::AnnotatedUserInput` and\nactivated for a turn. Broadcast to every subscribed client so TUI / GUI\ninstances show the same user line that reconnect snapshots replay.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Older mixed-version producers omit it.","type":["string","null"]},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"}},"required":["segments"],"type":"object"},"event":{"const":"user_message","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A canonical assistant/tool entry has committed to durable history.\nStreaming events remain the in-flight authority; this event supplies the\nstable identity used to reconcile that live block with paged history.","properties":{"data":{"additionalProperties":false,"properties":{"entry":{"$ref":"#/$defs/SessionSnapshotEntry"}},"required":["entry"],"type":"object"},"event":{"const":"session_entry_committed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One agent-injected system item committed to history.\n\nCarries the JSON form of `session_store::SystemItem`. Covers\n`Method::Notify` echoes, child-Worker lifecycle events from\n`Method::WorkerEvent`, `@` resolution payloads, and any future\nClients dispatch on the `kind` tag for typed rendering instead\nof parsing free-text prefixes like `[Notification] …` or\n`[File: …]`.\n\nOne event per `LogEntry::AnnotatedSystemItem` commit. Disk-side and\nwire-side are 1:1.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Synthetic compatibility events omit it.","type":["string","null"]},"item":true},"required":["item"],"type":"object"},"event":{"const":"system_item","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A new self-driving cycle has begun (IDLE → active transition).\n\nMarker event for the start of an Invoke range; the range extends\nimplicitly until the next `InvokeStart`. Fires for every accepted\n`Method::Submit` (kind=`UserSend`), `Method::Notify` (kind=`Notify`),\n`Method::WorkerEvent` re-injection (kind=`WorkerEvent`), and any other\nIDLE-breaking trigger. Mid-run interrupts (e.g. hook output,\ntyped system reminder insertion that doesn't break IDLE) do not\nemit `InvokeStart` — they appear as `SystemItem` only.\n\nCarries `kind` only; the payload (user text / notify message /\nworker event body) is delivered separately via the immediately\nfollowing `UserMessage` / `SystemItem` event.","properties":{"data":{"additionalProperties":false,"properties":{"kind":{"$ref":"#/$defs/InvokeKind"}},"required":["kind"],"type":"object"},"event":{"const":"invoke_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One AgentTurn boundary opened. An AgentTurn is a maximal run of\nLLM generation calls whose input messages are identical (i.e.\nretries from network errors / 5xx / stream disconnects collapse\ninto the same AgentTurn). When the input changes (a new tool\nresult lands, a user interrupts, etc.), the next LLM call belongs\nto a new AgentTurn.\n\n`turn` is the AgentTurn index from `Engine::turn_count`.\n\nCurrently retry is not yet implemented (`agen-stream-continuation`)\nso AgentTurn and `LlmCall` fire 1:1, but the contract here is\nthe AgentTurn boundary; consumers that want per-LLM-call signals\nmust subscribe to `LlmCallStart` / `LlmCallEnd` instead.","properties":{"data":{"additionalProperties":false,"properties":{"turn":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["turn"],"type":"object"},"event":{"const":"turn_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"AgentTurn closed.","properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/TurnResult"},"turn":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["turn","result"],"type":"object"},"event":{"const":"turn_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One LLM generation call started (1 request → 1 generation, retry\nincluded). Multiple `LlmCall*` pairs may fire inside a single\n`TurnStart` / `TurnEnd` pair when a request is retried; today\nthey fire 1:1 because retry is not implemented.\n\n`llm_call` is the worker-wide running counter from\n`Engine::llm_call_count`.","properties":{"data":{"additionalProperties":false,"properties":{"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call"],"type":"object"},"event":{"const":"llm_call_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"LLM generation call ended.","properties":{"data":{"additionalProperties":false,"properties":{"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call"],"type":"object"},"event":{"const":"llm_call_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A transport-level LLM request retry has been scheduled.\n\nThis is operational state for clients to render while the worker is\nwaiting in backoff. It is not part of conversation history.","properties":{"data":{"additionalProperties":false,"properties":{"elapsed_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"error":{"type":"string"},"failed_attempt":{"description":"The attempt that just failed. 1 origin.","format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"max_attempts":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"status":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":["integer","null"]},"wait_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call","failed_attempt","max_attempts","wait_ms","elapsed_ms","error"],"type":"object"},"event":{"const":"llm_retry","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Stream generation was interrupted after events had begun and the worker\nis continuing with a follow-up LLM request.","properties":{"data":{"additionalProperties":false,"properties":{"attempt":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"max_attempts":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"reason":{"type":"string"}},"required":["llm_call","attempt","max_attempts","reason"],"type":"object"},"event":{"const":"llm_continuation","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"text_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"text_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A reasoning / thinking block has started.\n\nAlways paired with a `ThinkingDone`. `ThinkingDelta` is optional:\nsome providers (or some configurations) emit thinking metadata\nwithout plaintext, in which case Start → Done arrive with no\ndeltas in between. Multiple thinking blocks per turn are allowed.","properties":{"event":{"const":"thinking_start","type":"string"}},"required":["event"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"thinking_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Thinking block completed. `text` is the full accumulated body\n(empty string when the provider didn't emit plaintext).","properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"thinking_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"event":{"const":"tool_call_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"id":{"type":"string"},"json":{"type":"string"}},"required":["id","json"],"type":"object"},"event":{"const":"tool_call_args_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name","arguments"],"type":"object"},"event":{"const":"tool_call_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"anyOf":[{"$ref":"#/$defs/ToolResultDisposition"},{"type":"null"}]},"id":{"type":"string"},"is_error":{"default":false,"type":"boolean"},"output":{"description":"Full tool output. Absent when the tool chose to return\nsummary-only, or when the result was pruned.","type":["string","null"]},"summary":{"description":"Short human-readable summary. Always present; used by clients\nthat only want a 1-line rendering (e.g. collapsed views).","type":"string"}},"required":["id","summary","is_error"],"type":"object"},"event":{"const":"tool_result","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Token accounting for one LLM request.\n\n`input_tokens` is the prompt prefix occupancy (cache reads /\ncache writes included), as normalised by the worker layer.\n`cache_read_input_tokens` is the cache-hit subset of that\noccupancy; subtracting it yields the \"net upload\" the client\nactually paid full price to send on this request, which is what\nthe TUI status line accumulates per turn.","properties":{"data":{"additionalProperties":false,"properties":{"cache_read_input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"type":"object"},"event":{"const":"usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Authoritative current-context occupancy after a non-request boundary.\n\nUnlike [`Event::Usage`], this is not cumulative request traffic. It lets\nconnected clients converge with reconnect snapshots after run completion,\ncompaction, rewind, and other history rewrites. `None` explicitly means\nthe current value is unavailable and stale occupancy must be cleared.","properties":{"data":{"additionalProperties":false,"properties":{"usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}]}},"type":"object"},"event":{"const":"context_usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/RunResult"}},"required":["result"],"type":"object"},"event":{"const":"run_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/ErrorCode"},"message":{"type":"string"}},"required":["code","message"],"type":"object"},"event":{"const":"error","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Sent exactly once at the start of every client connection.\n\n`entries` is the session-log mirror at subscribe time, serialised\nas the JSON form of `session_store::LogEntry`. This is the\nbulk-reconstruction lane: clients walk the entries to seed their\nderived view.\n\n`greeting` and `status` accompany the snapshot so clients render\nworker identity and current controller state without an extra round\ntrip.\n\nLive updates after the snapshot arrive through the streaming\nevents (`TextDelta` / `ToolCall*` / `ToolResult` / etc.) plus\nrole-specific entry events (`SegmentRotated` / `SystemItem`) —\nthere is no generic \"every committed entry\" broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"greeting":{"$ref":"#/$defs/Greeting"},"in_flight":{"$ref":"#/$defs/InFlightSnapshot","description":"Unfinished model output that has already streamed in the current\nrun but is not yet represented by committed snapshot entries."},"internal_workers":{"description":"Parent-owned Internal Worker sessions visible to this client.\nService-private Internal Workers are deliberately excluded.","items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"Full revisioned live execution state. `Stopped` remains Runtime\ncatalog authority and is deliberately not represented here."}},"required":["session","greeting","state"],"type":"object"},"event":{"const":"snapshot","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A live event from a parent-owned Internal Worker. The payload reuses the\nnormal Worker event vocabulary while the wrapper carries stable origin\nidentity and a per-child revision fence.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","revision","event"],"type":"object"},"event":{"const":"internal_worker","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Terminal removal fence for one parent-owned Internal Worker session.\n\nClients discard the matching child and descendants, then ignore later\nnested events for this identity until an authoritative snapshot replaces\nthe projection.","properties":{"data":{"additionalProperties":false,"properties":{"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","revision"],"type":"object"},"event":{"const":"internal_worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Server-side segment log rotated to a fresh `SegmentStart`.\n\nFires on compaction and on auto-fork when the store head drifts\nA compaction/fork has replaced the authoritative segment. Clients drop\ntheir derived view and reseed from the canonical committed snapshot.","properties":{"data":{"additionalProperties":false,"properties":{"session":{"$ref":"#/$defs/SessionSnapshot"}},"required":["session"],"type":"object"},"event":{"const":"segment_rotated","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Current Worker controller status. Broadcast on every controller-level\ntransition and included in `History` snapshots for late attach.","properties":{"data":{"additionalProperties":false,"properties":{"snapshot":{"$ref":"#/$defs/WorkerStateSnapshot"}},"required":["snapshot"],"type":"object"},"event":{"const":"worker_state","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"acknowledgement":{"$ref":"#/$defs/WorkerCommandAcknowledgement"}},"required":["acknowledgement"],"type":"object"},"event":{"const":"command_acknowledged","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Bounded, provider-owned command telemetry for the live Console. This is\nintentionally not a history entry and is reconstructed from\n`Snapshot.in_flight.commands` after reconnect.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/CommandEvent"}},"required":["event"],"type":"object"},"event":{"const":"command","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListCompletions`. Delivered only to the\nrequesting socket (not broadcast). `entries` is empty when no\ncandidates match or when the requested kind has no resolver\nwired up.","properties":{"data":{"additionalProperties":false,"properties":{"entries":{"items":{"$ref":"#/$defs/CompletionEntry"},"type":"array"},"kind":{"$ref":"#/$defs/CompletionKind"}},"required":["kind","entries"],"type":"object"},"event":{"const":"completions","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListRewindTargets`. Clients should only open a picker\nin response to their own pending request; the event may be broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"targets":{"items":{"$ref":"#/$defs/RewindTarget"},"type":"array"}},"required":["head_entries","targets"],"type":"object"},"event":{"const":"rewind_targets","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A rewind has truncated the authoritative session. `session` is the\nretained canonical snapshot clients should use to reseed display state.","properties":{"data":{"additionalProperties":false,"properties":{"input":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"summary":{"$ref":"#/$defs/RewindSummary"}},"required":["session","input","summary"],"type":"object"},"event":{"const":"rewind_applied","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListWorkers`. Payload is a stable JSON value so the Worker\ncrate can evolve discovery fields without introducing a protocol\ndependency on session-store.","properties":{"data":{"additionalProperties":false,"properties":{"workers":true},"required":["workers"],"type":"object"},"event":{"const":"workers_listed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::RestoreWorker`.","properties":{"data":{"additionalProperties":false,"properties":{"result":true},"required":["result"],"type":"object"},"event":{"const":"worker_restored","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::RegisterPeer`.","properties":{"data":{"additionalProperties":false,"properties":{"result":true},"required":["result"],"type":"object"},"event":{"const":"peer_registered","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"$ref":"#/$defs/Alert"},"event":{"const":"alert","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Latest memory extract/consolidation lifecycle event for UI observability.\n\nThis is not part of LLM history or prompt context; clients may display it\nbriefly as operational status.","properties":{"data":{"$ref":"#/$defs/MemoryWorkerEvent"},"event":{"const":"memory_worker","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Runtime-only compaction progress. `None` clears the current status.\nThis never enters Session history and carries no operation or Segment identity.","properties":{"data":{"additionalProperties":false,"properties":{"compaction":{"anyOf":[{"$ref":"#/$defs/InFlightCompaction"},{"type":"null"}]}},"type":"object"},"event":{"const":"compaction_progress","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Legacy compaction lifecycle event retained for wire read compatibility.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Compaction completed and the session was rotated.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Compaction failed or was cancelled. The session is unchanged.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_failed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"event":{"const":"shutdown","type":"string"}},"required":["event"],"type":"object"}]},"EventSubscriptionSelector":{"oneOf":[{"additionalProperties":false,"properties":{"topic":{"const":"runtime_workers","type":"string"}},"required":["topic"],"type":"object"},{"additionalProperties":false,"properties":{"topic":{"const":"worker_lifecycle","type":"string"},"worker_ids":{"$ref":"#/$defs/SubscriptionWorkerIds"}},"required":["topic","worker_ids"],"type":"object"},{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"topic":{"const":"worker_protocol","type":"string"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["topic","worker_id"],"type":"object"},{"additionalProperties":false,"description":"Server-derived Workspace projection. Workspace identity comes from the\nauthenticated connection and is deliberately absent from this selector.","properties":{"topic":{"const":"workspace_workers","type":"string"}},"required":["topic"],"type":"object"},{"additionalProperties":false,"description":"Typed extension point for the later Workdir subscription slice.","properties":{"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic"],"type":"object"}]},"Greeting":{"additionalProperties":false,"description":"Worker self-description rendered by clients when a session starts empty.\n\nSnapshot metadata contains only resolved public execution information so\nclients do not need their own copy of the manifest.","properties":{"context_tokens":{"default":0,"description":"Legacy numeric mirror retained for mixed-version clients.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"context_usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}],"description":"Typed current-context value. Older snapshots omit it; clients may fall\nback to `context_tokens` only when `context_window` is known."},"context_window":{"default":0,"description":"Resolved effective model context window in tokens. Legacy producers use\nzero when this is unavailable; clients must treat that as unknown.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"cwd":{"type":"string"},"model":{"type":"string"},"provider":{"type":"string"},"reasoning":{"anyOf":[{"$ref":"#/$defs/ReasoningConfig"},{"type":"null"}],"description":"Effective reasoning control. Absent means unspecified or unavailable."},"scope_summary":{"type":"string"},"tools":{"items":{"type":"string"},"type":"array"},"worker_name":{"type":"string"}},"required":["worker_name","cwd","provider","model","scope_summary","tools","context_tokens","context_window"],"type":"object"},"InFlightBlock":{"oneOf":[{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"thinking","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"type":"string"},"id":{"type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"state":{"$ref":"#/$defs/InFlightToolCallState"}},"required":["kind","id","name","args"],"type":"object"}]},"InFlightCompaction":{"additionalProperties":false,"properties":{"phase":{"$ref":"#/$defs/CompactionPhase"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"trigger":{"$ref":"#/$defs/CompactionTrigger"}},"required":["phase","started_at_ms","trigger"],"type":"object"},"InFlightSnapshot":{"additionalProperties":false,"description":"Unfinished model output and active command state included in\n`Event::Snapshot` for clients that attach while work is still streaming.\n\nThese blocks are presentation state only: they are reconstructed from the\nactive Worker controller and must not be treated as committed assistant\nhistory. Finalized assistant items continue to come from ordinary snapshot\nentries.","properties":{"blocks":{"items":{"$ref":"#/$defs/InFlightBlock"},"type":"array"},"commands":{"items":{"$ref":"#/$defs/CommandSnapshot"},"type":"array"},"compaction":{"anyOf":[{"$ref":"#/$defs/InFlightCompaction"},{"type":"null"}],"description":"The currently running compaction, if any.\n\nThis is lifecycle progress only. Candidate history and the staged\nSegment remain private until the Segment is activated atomically."}},"type":"object"},"InFlightToolCallState":{"enum":["pending","streaming_args","done"],"type":"string"},"InternalWorkerKind":{"description":"Presentation category for an Internal Worker exposed through its parent's\nprotocol stream. Internal Workers never become independently addressable\nprotocol subjects.","oneOf":[{"enum":["sub_worker"],"type":"string"},{"additionalProperties":false,"properties":{"service":{"additionalProperties":false,"properties":{"kind":{"type":"string"}},"required":["kind"],"type":"object"}},"required":["service"],"type":"object"}]},"InternalWorkerRef":{"additionalProperties":false,"description":"Stable presentation identity for one parent-owned Internal Worker session.\n`name` is display-only; `session_id` is the identity used by clients.","properties":{"kind":{"$ref":"#/$defs/InternalWorkerKind"},"name":{"type":"string"},"parent_session_id":{"type":["string","null"]},"session_id":{"type":"string"}},"required":["session_id","name","kind"],"type":"object"},"InternalWorkerSnapshot":{"additionalProperties":false,"description":"Reconnect state for one visible Internal Worker. The revision fences live\n`Event::InternalWorker` updates that raced with parent snapshot assembly.","properties":{"error":{"type":["string","null"]},"greeting":{"anyOf":[{"$ref":"#/$defs/Greeting"},{"type":"null"}],"description":"Public execution metadata for this child. Older snapshots and service\nworkers may omit it; clients must never substitute the parent's values."},"in_flight":{"$ref":"#/$defs/InFlightSnapshot"},"internal_workers":{"items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"session":{"$ref":"#/$defs/SessionSnapshot"},"status":{"$ref":"#/$defs/WorkerStatus","default":"idle"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","revision","session","status"],"type":"object"},"InvokeKind":{"description":"Kind of trigger that opened a new Invoke (IDLE → active) range.\n\nOne Invoke groups all entries from this trigger up to the next\n`Invoke` marker. The kind is the only payload — content (user text,\nnotify message, worker event body) is delivered by the immediately\nfollowing Turn entry, not by the marker itself.","oneOf":[{"const":"user_send","description":"`Method::Submit` — a user submission.","type":"string"},{"const":"notify","description":"`Method::Notify` — free-text notification injected into history.","type":"string"},{"const":"worker_event","description":"`Method::WorkerEvent` — typed lifecycle report from a child Worker.","type":"string"},{"const":"system_reminder","description":"A typed system reminder that crosses an IDLE boundary (mid-run\nreminders that don't break IDLE are SystemItem-only and do not\nopen a new Invoke).","type":"string"},{"const":"wakeup","description":"Cron / RemoteTrigger style scheduled wake-up. Reserved; no\nproducer is wired today but the variant is part of the initial\nset so future schedulers don't have to amend the wire enum.","type":"string"}]},"MemoryWorkerEvent":{"additionalProperties":false,"properties":{"message":{"description":"Human-readable compact form for actionbar rendering.","type":"string"},"reason":{"type":"string"},"run_id":{"type":"string"},"status":{"type":"string"},"timestamp_ms":{"description":"Milliseconds since the Unix epoch.","format":"int64","maximum":9007199254740991.0,"minimum":-9007199254740991.0,"type":"integer"},"trigger":{"type":"string"},"worker":{"type":"string"}},"required":["worker","status","run_id","trigger","reason","message","timestamp_ms"],"type":"object"},"Method":{"oneOf":[{"additionalProperties":false,"description":"Durably accept typed input for immediate activation or the session FIFO.\nRunning or Paused Workers queue Submit for a later turn; use Notify for\nadvisory information to incorporate into work already in progress.\n\n`submission_request_id` is generated by the authenticated caller and is\nused only for idempotent retry. Worker allocates the durable\n`submission_id` returned by [`Event::SubmissionAccepted`].","properties":{"method":{"const":"submit","type":"string"},"params":{"additionalProperties":false,"properties":{"input":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","input"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Human-readable text injected into the target Worker's LLM context as a\nnon-blocking system message. Accepted notifications are delivered at the\nnext between-turn boundary, or start a coalesced notification run while\nthe Worker is idle. No side effects beyond LLM context; use\n`WorkerEvent` for typed lifecycle reports.","properties":{"method":{"const":"notify","type":"string"},"params":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Typed lifecycle report from a child Worker to its direct parent.","properties":{"method":{"const":"worker_event","type":"string"},"params":{"$ref":"#/$defs/WorkerEvent"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Return the authoritative FIFO summary without exposing queued payloads.","properties":{"method":{"const":"list_pending_submissions","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Remove one queued submission. Running or already activated submissions\nare immutable and therefore cannot be cancelled here.","properties":{"method":{"const":"cancel_pending_submission","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"submission_id":{"type":"string"}},"required":["submission_id","expected_revision"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Remove every queued submission while preserving the active run.","properties":{"method":{"const":"clear_pending_submissions","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["expected_revision"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Activate the next queued submission while the Worker is idle. This is an\nexplicit recovery operation and never resumes a paused run implicitly.","properties":{"method":{"const":"continue_pending","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_head_id":{"type":"string"},"expected_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["expected_revision","expected_head_id"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"resume","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"cancel","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Stop the in-flight turn and transition to `Paused`.\n\nUnlike `Cancel` (which discards and returns to `Idle`), a paused\nWorker can resume the interrupted work via `Resume`, or accept a\nfresh `Submit` (orphan `tool_use` items are closed with a\nsynthetic tool result before the new user message is appended).","properties":{"method":{"const":"pause","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Request an explicit compaction while the Worker is otherwise idle.\n\nThis is a typed control method: clients must not send `compact` as a\n`Method::Submit` user message.","properties":{"method":{"const":"compact","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Ask the Worker to list valid rewind targets from its authoritative session log.","properties":{"method":{"const":"list_rewind_targets","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Truncate the current session back to the selected rewind target and\nreturn the selected user input to the client composer.","properties":{"method":{"const":"rewind_to","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"target":{"$ref":"#/$defs/RewindTargetId"}},"required":["target","expected_head_entries"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"shutdown","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Request a list of completion candidates from the Worker.\n\nReply is sent on the same socket as `Event::Completions` (not\nbroadcast). The IPC server handles this directly and writes\nthe response straight back to the requesting socket.","properties":{"method":{"const":"list_completions","type":"string"},"params":{"additionalProperties":false,"properties":{"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"type":"string"}},"required":["kind","prefix"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"List Workers visible to this Worker from durable Worker state and the spawned-child\nregistry. This is not a host-wide Worker universe query.","properties":{"method":{"const":"list_workers","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Restore a visible stopped/restorable Worker, or report that it is already\nlive. Missing state and not-visible state are distinct errors.","properties":{"method":{"const":"restore_worker","type":"string"},"params":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Register another existing Worker as a reciprocal peer of this Worker.\n\nThis is metadata/control state only: it does not ask the target's live\ncontroller for consent, and it must not grant delegated scope,\nspawned-child ownership, output cursors, or child lifecycle authority.","properties":{"method":{"const":"register_peer","type":"string"},"params":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}},"required":["method","params"],"type":"object"}]},"PasteArtifactAvailability":{"enum":["available","unavailable","integrity_failed"],"type":"string"},"PasteArtifactMediaType":{"description":"One typed piece of a user submission.\n\n`Method::Submit` and `Event::UserMessage` carry `Vec`. Dumb\nclients (CLI piping, scripts) only need to produce a single\n`Segment::Text`; richer clients (TUI / GUI) construct typed atoms\n(paste chips, file refs) and\nsend them through directly so the Worker side never has to re-parse a\nflattened string.\n\nForward compat: payloads with unknown `kind` deserialize to\n`Segment::Unknown`. Worker treats this the same as known-but-unresolved\nvariants — emits an alert and inserts a `[unknown input segment]`\nplaceholder into the LLM context so neither user nor LLM is blind to\nthe dropped intent.","enum":["text_plain_utf8"],"type":"string"},"PasteArtifactRef":{"additionalProperties":false,"description":"Session-owned reference to a large pasted-input artifact.\n\nThe reference contains only bounded integrity and provenance metadata. The\nartifact body remains in session storage and is available to the model only\nthrough the scoped paste-artifact tools installed by Worker.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/PasteArtifactAvailability","description":"Availability observed when this immutable reference was committed.\nReads revalidate storage and integrity rather than trusting this field."},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"char_count":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"line_count":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"media_type":{"$ref":"#/$defs/PasteArtifactMediaType"},"sha256":{"type":"string"},"source_entry_id":{"type":"string"}},"required":["artifact_id","created_at_ms","media_type","availability","byte_len","char_count","line_count","sha256","source_entry_id"],"type":"object"},"PendingSubmissionSummary":{"additionalProperties":false,"description":"Bounded public projection of one pending submission. Only a short display\npreview is exposed; full payload segments and provenance remain in the log.","properties":{"accepted_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"preview":{"description":"Single-line display text, at most 240 Unicode scalars plus an ellipsis.\nAbsent on older runtimes. Never used to reconstruct the submitted input.","type":["string","null"]},"segment_count":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"submission_id":{"type":"string"}},"required":["submission_id","accepted_at_ms","segment_count","byte_len"],"type":"object"},"PendingSubmissionsSnapshot":{"additionalProperties":false,"description":"Revisioned session-owned FIFO projection used by snapshots and live events.","properties":{"head_id":{"default":null,"type":["string","null"]},"notification_count":{"default":0,"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"notification_previews":{"description":"Ordered display-only previews of waiting notifications, each bounded like\nPendingSubmissionSummary::preview. Older runtimes omit this field.","items":{"type":"string"},"type":"array"},"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"submissions":{"default":[],"items":{"$ref":"#/$defs/PendingSubmissionSummary"},"type":"array"}},"required":["revision","head_id","notification_count","submissions"],"type":"object"},"Permission":{"description":"Permission lattice used by [`ScopeRule`].\n\nThe derived `Ord` instance follows declaration order, so\n`Read < Write`. Allow rules grant the stated level (and by extension\neverything below); deny rules cap the effective level **strictly\nbelow** the stated level.","enum":["read","write"],"type":"string"},"ReasoningConfig":{"description":"Public reasoning configuration resolved for the model client.\n\nThis deliberately exposes only the effective provider control, not the\nWorker manifest or provider credentials. The tagged shape prevents token\nbudgets from being presented as effort labels.","oneOf":[{"additionalProperties":false,"properties":{"effort":{"type":"string"},"kind":{"const":"effort","type":"string"}},"required":["kind","effort"],"type":"object"},{"additionalProperties":false,"properties":{"budget_tokens":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"},"kind":{"const":"budget_tokens","type":"string"}},"required":["kind","budget_tokens"],"type":"object"}]},"RewindSummary":{"additionalProperties":false,"properties":{"discarded_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_side_effect_warning":{"type":"boolean"},"truncated_to_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["truncated_to_entries","discarded_entries","tool_side_effect_warning"],"type":"object"},"RewindTarget":{"additionalProperties":false,"properties":{"disabled_reason":{"type":["string","null"]},"eligible":{"type":"boolean"},"expected_head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"id":{"$ref":"#/$defs/RewindTargetId"},"preview":{"type":"string"},"timestamp_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"truncate_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"turn_index":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"warning":{"type":["string","null"]}},"required":["id","expected_head_entries","truncate_entries","turn_index","preview","eligible"],"type":"object"},"RewindTargetId":{"additionalProperties":false,"properties":{"segment_id":{"type":"string"},"user_input_entry_index":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["segment_id","user_input_entry_index"],"type":"object"},"RunFailureKind":{"enum":["engine","compaction"],"type":"string"},"RunResult":{"oneOf":[{"enum":["finished","paused","limit_reached"],"type":"string"},{"const":"cancelled","description":"The logical Run ended because the user intentionally cancelled it after\nobservable work. Unlike `RolledBack`, committed history is retained.","type":"string"},{"const":"rolled_back","description":"The accepted Method::Submit produced no assistant/tool output before\nuser interruption, so the Worker rolled the submit-time turn state back\nto its pre-submit snapshot. Clients should treat the Worker as Idle and\nrestore the just-submitted input into the editable composer if desired.","type":"string"}]},"RunResumeSource":{"enum":["compaction","pause"],"type":"string"},"RunYieldReason":{"enum":["compaction"],"type":"string"},"ScopeRule":{"additionalProperties":false,"description":"A single allow or deny rule inside a scope configuration.","properties":{"permission":{"$ref":"#/$defs/Permission","description":"Permission level this rule grants (allow) or caps strictly below\n(deny)."},"recursive":{"default":true,"description":"When `false`, the rule only matches the target itself and its\ndirect children. Defaults to `true`.","type":"boolean"},"symlink_policy":{"$ref":"#/$defs/SymlinkPolicy","default":"resolved","description":"Which path identity an allow rule uses when symbolic links are\nencountered. Deny rules always inspect both identities."},"target":{"description":"Target path. Must be absolute by the time a `Scope` is built from\nthis rule — relative paths are resolved per-layer against the\nmanifest file's directory (cwd for overlay layers) before cascade\nmerge.","type":"string"}},"required":["target","permission","recursive","symlink_policy"],"type":"object"},"Segment":{"oneOf":[{"additionalProperties":false,"description":"Free-form text. The fallback every client can produce.","properties":{"content":{"type":"string"},"kind":{"const":"text","type":"string"}},"required":["kind","content"],"type":"object"},{"additionalProperties":false,"description":"Bracketed-paste capture from a TUI-style client. `id`, `chars`\nand `lines` carry the metadata needed to re-render a\n`[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage`\nre-broadcast.","properties":{"chars":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"content":{"type":"string"},"id":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"kind":{"const":"paste","type":"string"},"lines":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"}},"required":["kind","id","chars","lines","content"],"type":"object"},{"additionalProperties":false,"description":"Internal reference produced when Worker stores a large `Paste` before\ncommitting input. Clients may receive this in history/event projections;\nthe body is intentionally absent.","properties":{"artifact":{"$ref":"#/$defs/PasteArtifactRef"},"kind":{"const":"paste_artifact","type":"string"}},"required":["kind","artifact"],"type":"object"},{"additionalProperties":false,"description":"Client-local file uploaded into the owning Worker session before submit.\nThe Session Log stores only this immutable reference, never file bytes or\nthe client's local path.","properties":{"file":{"$ref":"#/$defs/UploadedFileRef"},"kind":{"const":"uploaded_file","type":"string"}},"required":["kind","file"],"type":"object"},{"additionalProperties":false,"description":"`@` file-system reference. Worker resolves readable files to\n`[File: ]` attachments and readable normal directories to shallow\n`[Dir: ]` listings; the flattened user text keeps the literal\n`@` placeholder either way.","properties":{"kind":{"const":"file_ref","type":"string"},"path":{"type":"string"}},"required":["kind","path"],"type":"object"},{"additionalProperties":false,"description":"Source-qualified request for the host to start a Flow before committing\nthis user input. Runtime resolves it through Workspace authority and\nreplaces it with the entered state's instructions before Worker-side\ninput resolution.","properties":{"kind":{"const":"flow","type":"string"},"selector":{"type":"string"}},"required":["kind","selector"],"type":"object"},{"additionalProperties":false,"description":"Unknown variant from a newer client. Worker treats this as an\nunresolved input — surfaces an alert and inserts a placeholder.\nRound-trip is lossy: re-serializing yields `{\"kind\":\"unknown\"}`.","properties":{"kind":{"const":"unknown","type":"string"}},"required":["kind"],"type":"object"}]},"SessionContentPart":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"refusal","type":"string"},"refusal":{"type":"string"}},"required":["kind","refusal"],"type":"object"}]},"SessionEntryProvenance":{"enum":["human_input","worker_input","flow_instruction","backend_instruction","model_output","tool_output","derived_summary","legacy_unknown"],"type":"string"},"SessionMessageRole":{"enum":["user","assistant"],"type":"string"},"SessionSnapshot":{"additionalProperties":false,"description":"Canonical, storage-independent projection of committed session history.\n\nWorker protocols expose this DTO instead of append-log records. New\nstorage variants can therefore be added without teaching every client how\nto replay the durable log format.","properties":{"entries":{"items":{"$ref":"#/$defs/SessionSnapshotEntry"},"type":"array"},"pending_submissions":{"$ref":"#/$defs/PendingSubmissionsSnapshot","default":{"head_id":null,"notification_count":0,"revision":0,"submissions":[]}}},"required":["entries","pending_submissions"],"type":"object"},"SessionSnapshotEntry":{"oneOf":[{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"user_input","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","segments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"items":{"$ref":"#/$defs/SessionContentPart"},"type":"array"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"message","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"role":{"$ref":"#/$defs/SessionMessageRole"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","role","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"call_id":{"type":"string"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","name","arguments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"attachments":{"items":{"$ref":"#/$defs/SessionToolAttachment"},"type":"array"},"call_id":{"type":"string"},"content":{"type":["string","null"]},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"is_error":{"type":"boolean"},"kind":{"const":"tool_result","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"summary":{"type":"string"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","summary","is_error","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"type":"string"},"data":true,"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"item_kind":{"type":"string"},"kind":{"const":"system_item","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","item_kind","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable, non-terminal control return from one logical Run. This is not a\ncompleted run and must not be used to infer the current Worker state.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_yielded","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"reason":{"$ref":"#/$defs/RunYieldReason"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","reason","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable boundary immediately before Engine execution resumes. The source\ndistinguishes automatic compaction continuation from an intentional\nuser-pause resume; it does not prove that later model output occurred.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_resumed","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"source":{"$ref":"#/$defs/RunResumeSource"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","source","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable terminal for an intentional cancellation. Consumers must not\npresent this lifecycle result as an execution failure.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_cancelled","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"failure":{"anyOf":[{"$ref":"#/$defs/RunFailureKind"},{"type":"null"}]},"kind":{"const":"run_error","type":"string"},"message":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","message","entry_id","timestamp","provenance"],"type":"object"}]},"SessionToolAttachment":{"additionalProperties":false,"properties":{"data_base64":{"description":"Base64-encoded durable attachment body. Public snapshots preserve the\ncommitted multimodal value instead of replacing it with placeholder text.","type":"string"},"media_type":{"type":"string"}},"required":["media_type","data_base64"],"type":"object"},"SubmissionDisposition":{"description":"Durable acceptance result for one idempotent Submit request.","oneOf":[{"enum":["started"],"type":"string"},{"const":"queued","description":"Input is durably held in the session FIFO until eligible for activation.","type":"string"}]},"SubscriptionEvent":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"payload":{"$ref":"#/$defs/SubscriptionEventPayload"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","payload"],"type":"object"},"event":{"const":"event","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionTerminationCode"},"message":{"type":"string"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","code","message"],"type":"object"},"event":{"const":"subscription_closed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionEventPayload":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"worker":{"$ref":"#/$defs/SubscriptionWorker"}},"required":["worker"],"type":"object"},"event":{"const":"worker_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id"],"type":"object"},"event":{"const":"worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","event"],"type":"object"},"event":{"const":"worker_protocol","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdir":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"}},"required":["workdir"],"type":"object"},"event":{"const":"workdir_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id"],"type":"object"},"event":{"const":"workdir_removed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionId":{"type":"string"},"SubscriptionRejectionCode":{"enum":["invalid_request","unsupported_protocol_version","unsupported_selector","unauthorized","resource_not_found","capacity_exceeded","internal"],"type":"string"},"SubscriptionRequest":{"oneOf":[{"additionalProperties":false,"properties":{"method":{"const":"subscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"}},"required":["request_id","selector"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"unsubscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"}},"required":["method","params"],"type":"object"}]},"SubscriptionRequestId":{"type":"string"},"SubscriptionResponse":{"oneOf":[{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"},"snapshot":{"$ref":"#/$defs/SubscriptionSnapshot"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id","selector","snapshot"],"type":"object"},"result":{"const":"subscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"},"result":{"const":"unsubscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionRejectionCode"},"message":{"type":"string"},"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"anyOf":[{"$ref":"#/$defs/SubscriptionId"},{"type":"null"}]}},"required":["request_id","code","message"],"type":"object"},"result":{"const":"subscription_rejected","type":"string"}},"required":["result","payload"],"type":"object"}]},"SubscriptionSnapshot":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workers":{"items":{"$ref":"#/$defs/SubscriptionWorker"},"type":"array"}},"required":["workers"],"type":"object"},"topic":{"const":"workers","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"events":{"items":{"$ref":"#/$defs/Event"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","events"],"type":"object"},"topic":{"const":"worker_protocol","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdirs":{"items":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"},"type":"array"}},"required":["workdirs"],"type":"object"},"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic","data"],"type":"object"}]},"SubscriptionTerminationCode":{"enum":["lagged","resource_gone","unauthorized","server_shutdown"],"type":"string"},"SubscriptionWorkdirId":{"type":"string"},"SubscriptionWorker":{"additionalProperties":false,"properties":{"availability":{"$ref":"#/$defs/SubscriptionWorkerAvailability","default":"observed","description":"Freshness of the Runtime-backed observation carried by this projection.\n`Unavailable` preserves catalog membership without claiming that execution stopped."},"display_name":{"type":["string","null"]},"has_running_internal_workers":{"default":false,"type":"boolean"},"job":{"anyOf":[{"$ref":"#/$defs/SubscriptionWorkerJob"},{"type":"null"}],"description":"Backend-owned Job binding. Runtime producers leave this unset; the\nWorkspace projection enriches both snapshots and live updates."},"profile":{"type":["string","null"]},"resource_key":{"description":"Workspace-scoped canonical resource key. Runtime producers leave this unset;\nWorkspace-facing projections must populate it before publishing the Worker.","type":["string","null"]},"runtime_id":{"description":"Set by the Workspace Server when projecting a Runtime-owned Worker to clients.\nRuntime producers leave this unset because the connection identifies the Runtime.","type":["string","null"]},"state":{"$ref":"#/$defs/SubscriptionWorkerState","description":"Runtime catalog lifecycle compatibility projection; not foreground-state authority."},"workdir_attachments":{"items":{"$ref":"#/$defs/SubscriptionWorkerWorkdirAttachment"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"},"worker_state":{"anyOf":[{"$ref":"#/$defs/WorkerStateSnapshot"},{"type":"null"}],"description":"Latest foreground state observed from the Worker. This remains\nabsent until an authoritative Worker snapshot/event has been applied."},"workspace_id":{"type":["string","null"]}},"required":["worker_id","state","availability","has_running_internal_workers"],"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"},"SubscriptionWorkerId":{"type":"string"},"SubscriptionWorkerIds":{"description":"A bounded, canonical worker-id set. Ordering on the wire and in equality/hash\nsemantics is lexical, so equivalent selectors aggregate to one upstream key.","items":{"$ref":"#/$defs/SubscriptionWorkerId"},"type":"array"},"SubscriptionWorkerJob":{"additionalProperties":false,"properties":{"attempt_id":{"type":"string"},"job_id":{"type":"string"},"purpose":{"type":"string"}},"required":["job_id","attempt_id","purpose"],"type":"object"},"SubscriptionWorkerProtocolMethod":{"additionalProperties":false,"properties":{"method":{"$ref":"#/$defs/Method"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","method"],"type":"object"},"SubscriptionWorkerState":{"enum":["idle","running","paused","stopped"],"type":"string"},"SubscriptionWorkerWorkdirAttachment":{"additionalProperties":false,"properties":{"alias":{"description":"Stable Worker-local routing alias.","type":"string"},"repository_key":{"type":["string","null"]},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["alias","working_directory_id"],"type":"object"},"SymlinkPolicy":{"description":"Symbolic-link identity used by one filesystem allow rule.\n\n`Resolved` is the least authority and the default: access is matched\nagainst the provider-resolved target. `Logical` intentionally grants the\npath as presented through the Workdir, even when it aliases another target.","enum":["resolved","logical"],"type":"string"},"ToolResultDisposition":{"enum":["success","error","interrupted","cancelled","outcome_unknown"],"type":"string"},"TurnResult":{"enum":["finished","paused"],"type":"string"},"UploadedFileAvailability":{"description":"Availability recorded for an uploaded client-local file.","enum":["available","unavailable","integrity_failed"],"type":"string"},"UploadedFileRef":{"additionalProperties":false,"description":"Session-owned immutable reference to a client-local uploaded file.\n\nUpload transports return an unbound reference. Worker fills\n`source_entry_id` immediately before the containing user input is committed;\ncommitted Session Log and public snapshot records therefore always retain\nthe durable source-entry identity without storing the file body.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/UploadedFileAvailability"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"file_name":{"type":"string"},"media_type":{"type":"string"},"sha256":{"type":"string"},"source_entry_id":{"type":["string","null"]}},"required":["artifact_id","file_name","media_type","created_at_ms","availability","byte_len","sha256"],"type":"object"},"WorkerBusyState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"run","type":"string"},"state":{"$ref":"#/$defs/WorkerRunState"}},"required":["kind","state"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"maintenance","type":"string"},"state":{"$ref":"#/$defs/WorkerMaintenanceState"}},"required":["kind","state"],"type":"object"}]},"WorkerCommandAcknowledgement":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandKind"},"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"disposition":{"$ref":"#/$defs/WorkerCommandDisposition"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"The complete authoritative state observed after command admission."}},"required":["command_id","command","disposition","state"],"type":"object"},"WorkerCommandDisposition":{"enum":["accepted","stale_command_id","conflict","invalid_state"],"type":"string"},"WorkerCommandEnvelope":{"additionalProperties":false,"description":"Caller-owned identity for one state-changing Worker command.\n\nA controller accepts command ids in strictly increasing order. Exact retries\nof an accepted id must retain the same command kind.","properties":{"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["command_id"],"type":"object"},"WorkerCommandKind":{"enum":["resume","cancel","pause","compact","shutdown"],"type":"string"},"WorkerEvent":{"description":"Typed lifecycle events sent from a child Worker to its parent.\n\nDelivered as `Method::WorkerEvent` over the parent's Unix socket. The\nparent Controller always applies variant-specific side effects\n(registry / worker-allocation updates). Agent-visible variants are also\nqueued into the notification buffer; control-plane-only variants are\nnot injected into the parent's LLM context.\n\nTransport is fire-and-forget; receivers must tolerate out-of-order\ndelivery (e.g. `TurnEnded` arriving after `ShutDown` for the same\nchild Worker).","oneOf":[{"additionalProperties":false,"description":"Child finished one turn and is back to IDLE.","properties":{"kind":{"const":"turn_ended","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Engine execution error occurred inside the child's turn.\n\nLimited to worker runtime failures (provider / tool errors) —\ndoes not include transient method-rejection responses such as\n`AlreadyRunning`.","properties":{"kind":{"const":"errored","type":"string"},"message":{"type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name","message"],"type":"object"},{"additionalProperties":false,"description":"Child has stopped (controller loop is exiting).","properties":{"kind":{"const":"shut_down","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Child SubWorker sub-delegated scope to a grandchild SubWorker via `SubWorkerSpawn`.\n\nControl-plane only: receivers apply registry side effects and\npropagate upward, but do not expose this as an agent notification.\n\nThe parent uses this to add the grandchild to its own\n`spawned_workers.json` so it can manage the grandchild directly\neven if the intermediate child dies. The parent then re-fires\nthis event upward (if it has a parent of its own) to maintain\nthe chain to root.","properties":{"kind":{"const":"scope_sub_delegated","type":"string"},"parent_worker":{"description":"Sub-delegating Worker (= the sender itself).","type":"string"},"scope":{"description":"Scope delegated to the grandchild.","items":{"$ref":"#/$defs/ScopeRule"},"type":"array"},"sub_socket":{"description":"Unix-socket path where the grandchild is reachable.","type":"string"},"sub_worker":{"description":"Name of the grandchild Worker.","type":"string"}},"required":["kind","parent_worker","sub_worker","sub_socket","scope"],"type":"object"}]},"WorkerMaintenanceState":{"enum":["compacting"],"type":"string"},"WorkerRunState":{"enum":["running","pausing","paused","cancelling"],"type":"string"},"WorkerState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"idle","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"busy","type":"string"},"state":{"$ref":"#/$defs/WorkerBusyState"}},"required":["kind","state"],"type":"object"}]},"WorkerStateSnapshot":{"additionalProperties":false,"properties":{"last_command_id":{"description":"Highest lifecycle command id observed by this controller instance.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"state":{"$ref":"#/$defs/WorkerState"}},"required":["last_command_id","state"],"type":"object"},"WorkerStatus":{"enum":["idle","running","paused","stopped"],"type":"string"},"WorkspaceSubscriptionWorkdir":{"additionalProperties":false,"description":"Workspace-facing Workdir summary. Backend-generated Repository UUIDs never\nenter this DTO; Workspace Server must resolve the required Repository key.","properties":{"repository_key":{"type":"string"},"state":{"type":"string"},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id","repository_key","state"],"type":"object"}},"$schema":"https://json-schema.org/draft/2020-12/schema","description":"Direction is explicit at the outer envelope, while concrete message kinds\nremain typed within each lane.","oneOf":[{"additionalProperties":false,"properties":{"frame":{"const":"request","type":"string"},"message":{"$ref":"#/$defs/SubscriptionRequest"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"response","type":"string"},"message":{"$ref":"#/$defs/SubscriptionResponse"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"event","type":"string"},"message":{"$ref":"#/$defs/SubscriptionEvent"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"worker_protocol","type":"string"},"message":{"$ref":"#/$defs/SubscriptionWorkerProtocolMethod"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"}],"title":"SubscriptionFrame"}; +const subscriptionFrameSchema: unknown = {"$defs":{"Alert":{"additionalProperties":false,"description":"User-facing alert emitted from the Worker layer.\n\nThis is a separate channel from `tracing` (developer logs): entries\nhere are assembled explicitly by the Worker when a condition should be\nsurfaced to the person driving the client. Keep messages short and\nhuman-readable.","properties":{"level":{"$ref":"#/$defs/AlertLevel"},"message":{"type":"string"},"source":{"$ref":"#/$defs/AlertSource"},"timestamp_ms":{"description":"Milliseconds since the Unix epoch.","format":"int64","maximum":9007199254740991.0,"minimum":-9007199254740991.0,"type":"integer"}},"required":["level","source","message","timestamp_ms"],"type":"object"},"AlertLevel":{"enum":["warn","error"],"type":"string"},"AlertSource":{"enum":["worker","engine","compactor","agents_md"],"type":"string"},"CommandEvent":{"oneOf":[{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"kind":{"const":"started","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_call_id":{"type":["string","null"]}},"required":["kind","command_id","observed_at_ms"],"type":"object"},{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"content":{"type":"string"},"end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"kind":{"const":"output","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"start_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"stream":{"$ref":"#/$defs/CommandStream"}},"required":["kind","command_id","stream","start_offset","end_offset","content","observed_at_ms"],"type":"object"},{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"exit_code":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":["integer","null"]},"kind":{"const":"terminal","type":"string"},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"status":{"$ref":"#/$defs/CommandStatus"},"stderr_end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"stdout_end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","command_id","status","stdout_end_offset","stderr_end_offset","observed_at_ms"],"type":"object"}]},"CommandSnapshot":{"additionalProperties":false,"properties":{"command_id":{"type":"string"},"exit_code":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":["integer","null"]},"last_output_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"observed_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"status":{"$ref":"#/$defs/CommandStatus"},"stderr":{"$ref":"#/$defs/CommandStreamSlice"},"stdout":{"$ref":"#/$defs/CommandStreamSlice"},"tool_call_id":{"type":["string","null"]}},"required":["command_id","status","started_at_ms","observed_at_ms","stdout","stderr"],"type":"object"},"CommandStatus":{"description":"Live provider-owned command status. These values are operational Console\nstate only and are never appended to Worker history.","enum":["running","completed","failed","timed_out","cancelled"],"type":"string"},"CommandStream":{"enum":["stdout","stderr"],"type":"string"},"CommandStreamSlice":{"additionalProperties":false,"properties":{"content":{"type":"string"},"end_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"start_offset":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"truncated":{"type":"boolean"}},"required":["start_offset","end_offset","content","truncated"],"type":"object"},"CompactionLifecycle":{"additionalProperties":false,"description":"Stable parent-owned lifecycle for one compaction run.","properties":{"compaction_id":{"type":"string"},"ended_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"error":{"type":["string","null"]},"internal_worker":{"anyOf":[{"$ref":"#/$defs/InternalWorkerRef"},{"type":"null"}]},"new_segment_id":{"type":["string","null"]},"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"schema_version":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"started_at_ms":{"description":"Milliseconds since the Unix epoch.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"state":{"$ref":"#/$defs/CompactionLifecycleState"},"summary":{"type":["string","null"]}},"required":["schema_version","compaction_id","revision","state","started_at_ms"],"type":"object"},"CompactionLifecycleState":{"enum":["running","done","failed","interrupted"],"type":"string"},"CompactionPhase":{"enum":["preparing","summarizing","committing"],"type":"string"},"CompactionTrigger":{"enum":["manual","pre_run","request_threshold"],"type":"string"},"CompletionEntry":{"additionalProperties":false,"description":"One completion candidate for a prefix query.\n\n`value` is a path (file kind).","properties":{"is_dir":{"default":false,"type":"boolean"},"value":{"type":"string"}},"required":["value","is_dir"],"type":"object"},"CompletionKind":{"description":"Kind of completion requested by `Method::ListCompletions`.\n\nMirrors the completion prefix sigil: `@` → `File`.","enum":["file"],"type":"string"},"ContextTokenSource":{"description":"Provenance of the current-context token count in a [`Greeting`].","oneOf":[{"const":"measured","description":"Provider-reported input occupancy from the most recent LLM request.","type":"string"},{"const":"estimated","description":"Current history projected from the Worker's token accounting records.","type":"string"}]},"ContextUsage":{"additionalProperties":false,"description":"Current prompt-context occupancy, distinct from cumulative run traffic.","properties":{"source":{"$ref":"#/$defs/ContextTokenSource"},"tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["tokens","source"],"type":"object"},"ErrorCode":{"enum":["already_running","not_running","not_paused","provider_error","tool_error","invalid_request","internal"],"type":"string"},"Event":{"oneOf":[{"additionalProperties":false,"description":"Durable Submit acceptance. A `Queued` receipt follows the durable FIFO\ncheckpoint. A `Started` receipt follows the pending activation checkpoint;\nthe corresponding [`Event::UserMessage`] marks the later history commit.\nRepeating the same request id and exact payload returns the same receipt\nwithout appending or activating twice.","properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"$ref":"#/$defs/SubmissionDisposition"},"submission_id":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","submission_id","disposition"],"type":"object"},"event":{"const":"submission_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","message"],"type":"object"},"event":{"const":"submission_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Durable notification acceptance. The request identity is persisted with\nthe notification receipt before this event is emitted. Repeating the\nsame request id and exact payload returns the same acknowledgement\nwithout appending the notification twice.","properties":{"data":{"additionalProperties":false,"properties":{"notification_request_id":{"type":"string"}},"required":["notification_request_id"],"type":"object"},"event":{"const":"notification_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated notification rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"},"event":{"const":"notification_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Revisioned FIFO replacement following enqueue, activation, cancel, or clear.","properties":{"data":{"additionalProperties":false,"properties":{"pending":{"$ref":"#/$defs/PendingSubmissionsSnapshot"}},"required":["pending"],"type":"object"},"event":{"const":"pending_submissions_changed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A user input message persisted as `LogEntry::AnnotatedUserInput` and\nactivated for a turn. Broadcast to every subscribed client so TUI / GUI\ninstances show the same user line that reconnect snapshots replay.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Older mixed-version producers omit it.","type":["string","null"]},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"}},"required":["segments"],"type":"object"},"event":{"const":"user_message","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A canonical assistant/tool entry has committed to durable history.\nStreaming events remain the in-flight authority; this event supplies the\nstable identity used to reconcile that live block with paged history.","properties":{"data":{"additionalProperties":false,"properties":{"entry":{"$ref":"#/$defs/SessionSnapshotEntry"}},"required":["entry"],"type":"object"},"event":{"const":"session_entry_committed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One agent-injected system item committed to history.\n\nCarries the JSON form of `session_store::SystemItem`. Covers\n`Method::Notify` echoes, child-Worker lifecycle events from\n`Method::WorkerEvent`, `@` resolution payloads, and any future\nClients dispatch on the `kind` tag for typed rendering instead\nof parsing free-text prefixes like `[Notification] …` or\n`[File: …]`.\n\nOne event per `LogEntry::AnnotatedSystemItem` commit. Disk-side and\nwire-side are 1:1.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Synthetic compatibility events omit it.","type":["string","null"]},"item":true},"required":["item"],"type":"object"},"event":{"const":"system_item","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A new self-driving cycle has begun (IDLE → active transition).\n\nMarker event for the start of an Invoke range; the range extends\nimplicitly until the next `InvokeStart`. Fires for every accepted\n`Method::Submit` (kind=`UserSend`), `Method::Notify` (kind=`Notify`),\n`Method::WorkerEvent` re-injection (kind=`WorkerEvent`), and any other\nIDLE-breaking trigger. Mid-run interrupts (e.g. hook output,\ntyped system reminder insertion that doesn't break IDLE) do not\nemit `InvokeStart` — they appear as `SystemItem` only.\n\nCarries `kind` only; the payload (user text / notify message /\nworker event body) is delivered separately via the immediately\nfollowing `UserMessage` / `SystemItem` event.","properties":{"data":{"additionalProperties":false,"properties":{"kind":{"$ref":"#/$defs/InvokeKind"}},"required":["kind"],"type":"object"},"event":{"const":"invoke_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One AgentTurn boundary opened. An AgentTurn is a maximal run of\nLLM generation calls whose input messages are identical (i.e.\nretries from network errors / 5xx / stream disconnects collapse\ninto the same AgentTurn). When the input changes (a new tool\nresult lands, a user interrupts, etc.), the next LLM call belongs\nto a new AgentTurn.\n\n`turn` is the AgentTurn index from `Engine::turn_count`.\n\nCurrently retry is not yet implemented (`agen-stream-continuation`)\nso AgentTurn and `LlmCall` fire 1:1, but the contract here is\nthe AgentTurn boundary; consumers that want per-LLM-call signals\nmust subscribe to `LlmCallStart` / `LlmCallEnd` instead.","properties":{"data":{"additionalProperties":false,"properties":{"turn":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["turn"],"type":"object"},"event":{"const":"turn_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"AgentTurn closed.","properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/TurnResult"},"turn":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["turn","result"],"type":"object"},"event":{"const":"turn_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One LLM generation call started (1 request → 1 generation, retry\nincluded). Multiple `LlmCall*` pairs may fire inside a single\n`TurnStart` / `TurnEnd` pair when a request is retried; today\nthey fire 1:1 because retry is not implemented.\n\n`llm_call` is the worker-wide running counter from\n`Engine::llm_call_count`.","properties":{"data":{"additionalProperties":false,"properties":{"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call"],"type":"object"},"event":{"const":"llm_call_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"LLM generation call ended.","properties":{"data":{"additionalProperties":false,"properties":{"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call"],"type":"object"},"event":{"const":"llm_call_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A transport-level LLM request retry has been scheduled.\n\nThis is operational state for clients to render while the worker is\nwaiting in backoff. It is not part of conversation history.","properties":{"data":{"additionalProperties":false,"properties":{"elapsed_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"error":{"type":"string"},"failed_attempt":{"description":"The attempt that just failed. 1 origin.","format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"max_attempts":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"status":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":["integer","null"]},"wait_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["llm_call","failed_attempt","max_attempts","wait_ms","elapsed_ms","error"],"type":"object"},"event":{"const":"llm_retry","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Stream generation was interrupted after events had begun and the worker\nis continuing with a follow-up LLM request.","properties":{"data":{"additionalProperties":false,"properties":{"attempt":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"llm_call":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"max_attempts":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"reason":{"type":"string"}},"required":["llm_call","attempt","max_attempts","reason"],"type":"object"},"event":{"const":"llm_continuation","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"text_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"text_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A reasoning / thinking block has started.\n\nAlways paired with a `ThinkingDone`. `ThinkingDelta` is optional:\nsome providers (or some configurations) emit thinking metadata\nwithout plaintext, in which case Start → Done arrive with no\ndeltas in between. Multiple thinking blocks per turn are allowed.","properties":{"event":{"const":"thinking_start","type":"string"}},"required":["event"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"thinking_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Thinking block completed. `text` is the full accumulated body\n(empty string when the provider didn't emit plaintext).","properties":{"data":{"additionalProperties":false,"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"},"event":{"const":"thinking_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"event":{"const":"tool_call_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"id":{"type":"string"},"json":{"type":"string"}},"required":["id","json"],"type":"object"},"event":{"const":"tool_call_args_delta","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name","arguments"],"type":"object"},"event":{"const":"tool_call_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"anyOf":[{"$ref":"#/$defs/ToolResultDisposition"},{"type":"null"}]},"id":{"type":"string"},"is_error":{"default":false,"type":"boolean"},"output":{"description":"Full tool output. Absent when the tool chose to return\nsummary-only, or when the result was pruned.","type":["string","null"]},"summary":{"description":"Short human-readable summary. Always present; used by clients\nthat only want a 1-line rendering (e.g. collapsed views).","type":"string"}},"required":["id","summary","is_error"],"type":"object"},"event":{"const":"tool_result","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Token accounting for one LLM request.\n\n`input_tokens` is the prompt prefix occupancy (cache reads /\ncache writes included), as normalised by the worker layer.\n`cache_read_input_tokens` is the cache-hit subset of that\noccupancy; subtracting it yields the \"net upload\" the client\nactually paid full price to send on this request, which is what\nthe TUI status line accumulates per turn.","properties":{"data":{"additionalProperties":false,"properties":{"cache_read_input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"type":"object"},"event":{"const":"usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Authoritative current-context occupancy after a non-request boundary.\n\nUnlike [`Event::Usage`], this is not cumulative request traffic. It lets\nconnected clients converge with reconnect snapshots after run completion,\ncompaction, rewind, and other history rewrites. `None` explicitly means\nthe current value is unavailable and stale occupancy must be cleared.","properties":{"data":{"additionalProperties":false,"properties":{"usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}]}},"type":"object"},"event":{"const":"context_usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/RunResult"}},"required":["result"],"type":"object"},"event":{"const":"run_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/ErrorCode"},"message":{"type":"string"}},"required":["code","message"],"type":"object"},"event":{"const":"error","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Sent exactly once at the start of every client connection.\n\n`entries` is the session-log mirror at subscribe time, serialised\nas the JSON form of `session_store::LogEntry`. This is the\nbulk-reconstruction lane: clients walk the entries to seed their\nderived view.\n\n`greeting` and `status` accompany the snapshot so clients render\nworker identity and current controller state without an extra round\ntrip.\n\nLive updates after the snapshot arrive through the streaming\nevents (`TextDelta` / `ToolCall*` / `ToolResult` / etc.) plus\nrole-specific entry events (`SegmentRotated` / `SystemItem`) —\nthere is no generic \"every committed entry\" broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"greeting":{"$ref":"#/$defs/Greeting"},"in_flight":{"$ref":"#/$defs/InFlightSnapshot","description":"Unfinished model output that has already streamed in the current\nrun but is not yet represented by committed snapshot entries."},"internal_workers":{"description":"Parent-owned Internal Worker sessions visible to this client.\nService-private Internal Workers are deliberately excluded.","items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"Full revisioned live execution state. `Stopped` remains Runtime\ncatalog authority and is deliberately not represented here."}},"required":["session","greeting","state"],"type":"object"},"event":{"const":"snapshot","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A live event from a parent-owned Internal Worker. The payload reuses the\nnormal Worker event vocabulary while the wrapper carries stable origin\nidentity and a per-child revision fence.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","revision","event"],"type":"object"},"event":{"const":"internal_worker","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Terminal removal fence for one parent-owned Internal Worker session.\n\nClients discard the matching child and descendants, then ignore later\nnested events for this identity until an authoritative snapshot replaces\nthe projection.","properties":{"data":{"additionalProperties":false,"properties":{"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","revision"],"type":"object"},"event":{"const":"internal_worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Server-side segment log rotated to a fresh `SegmentStart`.\n\nFires on compaction and on auto-fork when the store head drifts\nA compaction/fork has replaced the authoritative segment. Clients drop\ntheir derived view and reseed from the canonical committed snapshot.","properties":{"data":{"additionalProperties":false,"properties":{"session":{"$ref":"#/$defs/SessionSnapshot"}},"required":["session"],"type":"object"},"event":{"const":"segment_rotated","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Current Worker controller status. Broadcast on every controller-level\ntransition and included in `History` snapshots for late attach.","properties":{"data":{"additionalProperties":false,"properties":{"snapshot":{"$ref":"#/$defs/WorkerStateSnapshot"}},"required":["snapshot"],"type":"object"},"event":{"const":"worker_state","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"acknowledgement":{"$ref":"#/$defs/WorkerCommandAcknowledgement"}},"required":["acknowledgement"],"type":"object"},"event":{"const":"command_acknowledged","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Bounded, provider-owned command telemetry for the live Console. This is\nintentionally not a history entry and is reconstructed from\n`Snapshot.in_flight.commands` after reconnect.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/CommandEvent"}},"required":["event"],"type":"object"},"event":{"const":"command","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListCompletions`. Delivered only to the\nrequesting socket (not broadcast). `entries` is empty when no\ncandidates match or when the requested kind has no resolver\nwired up.","properties":{"data":{"additionalProperties":false,"properties":{"entries":{"items":{"$ref":"#/$defs/CompletionEntry"},"type":"array"},"kind":{"$ref":"#/$defs/CompletionKind"}},"required":["kind","entries"],"type":"object"},"event":{"const":"completions","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListRewindTargets`. Clients should only open a picker\nin response to their own pending request; the event may be broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"targets":{"items":{"$ref":"#/$defs/RewindTarget"},"type":"array"}},"required":["head_entries","targets"],"type":"object"},"event":{"const":"rewind_targets","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A rewind has truncated the authoritative session. `session` is the\nretained canonical snapshot clients should use to reseed display state.","properties":{"data":{"additionalProperties":false,"properties":{"input":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"summary":{"$ref":"#/$defs/RewindSummary"}},"required":["session","input","summary"],"type":"object"},"event":{"const":"rewind_applied","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::ListWorkers`. Payload is a stable JSON value so the Worker\ncrate can evolve discovery fields without introducing a protocol\ndependency on session-store.","properties":{"data":{"additionalProperties":false,"properties":{"workers":true},"required":["workers"],"type":"object"},"event":{"const":"workers_listed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::RestoreWorker`.","properties":{"data":{"additionalProperties":false,"properties":{"result":true},"required":["result"],"type":"object"},"event":{"const":"worker_restored","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Reply to `Method::RegisterPeer`.","properties":{"data":{"additionalProperties":false,"properties":{"result":true},"required":["result"],"type":"object"},"event":{"const":"peer_registered","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"$ref":"#/$defs/Alert"},"event":{"const":"alert","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Latest memory extract/consolidation lifecycle event for UI observability.\n\nThis is not part of LLM history or prompt context; clients may display it\nbriefly as operational status.","properties":{"data":{"$ref":"#/$defs/MemoryWorkerEvent"},"event":{"const":"memory_worker","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Runtime-only compaction progress. `None` clears the current status.\nThis never enters Session history and carries no operation or Segment identity.","properties":{"data":{"additionalProperties":false,"properties":{"compaction":{"anyOf":[{"$ref":"#/$defs/InFlightCompaction"},{"type":"null"}]}},"type":"object"},"event":{"const":"compaction_progress","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Legacy compaction lifecycle event retained for wire read compatibility.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_start","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Compaction completed and the session was rotated.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_done","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Compaction failed or was cancelled. The session is unchanged.","properties":{"data":{"additionalProperties":false,"properties":{"lifecycle":{"$ref":"#/$defs/CompactionLifecycle"}},"required":["lifecycle"],"type":"object"},"event":{"const":"compact_failed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"event":{"const":"shutdown","type":"string"}},"required":["event"],"type":"object"}]},"EventSubscriptionSelector":{"oneOf":[{"additionalProperties":false,"properties":{"topic":{"const":"runtime_workers","type":"string"}},"required":["topic"],"type":"object"},{"additionalProperties":false,"properties":{"topic":{"const":"worker_lifecycle","type":"string"},"worker_ids":{"$ref":"#/$defs/SubscriptionWorkerIds"}},"required":["topic","worker_ids"],"type":"object"},{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"topic":{"const":"worker_protocol","type":"string"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["topic","worker_id"],"type":"object"},{"additionalProperties":false,"description":"Server-derived Workspace projection. Workspace identity comes from the\nauthenticated connection and is deliberately absent from this selector.","properties":{"topic":{"const":"workspace_workers","type":"string"}},"required":["topic"],"type":"object"},{"additionalProperties":false,"description":"Typed extension point for the later Workdir subscription slice.","properties":{"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic"],"type":"object"}]},"Greeting":{"additionalProperties":false,"description":"Worker self-description rendered by clients when a session starts empty.\n\nSnapshot metadata contains only resolved public execution information so\nclients do not need their own copy of the manifest.","properties":{"context_tokens":{"default":0,"description":"Legacy numeric mirror retained for mixed-version clients.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"context_usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}],"description":"Typed current-context value. Older snapshots omit it; clients may fall\nback to `context_tokens` only when `context_window` is known."},"context_window":{"default":0,"description":"Resolved effective model context window in tokens. Legacy producers use\nzero when this is unavailable; clients must treat that as unknown.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"cwd":{"type":"string"},"model":{"type":"string"},"provider":{"type":"string"},"reasoning":{"anyOf":[{"$ref":"#/$defs/ReasoningConfig"},{"type":"null"}],"description":"Effective reasoning control. Absent means unspecified or unavailable."},"scope_summary":{"type":"string"},"tools":{"items":{"type":"string"},"type":"array"},"worker_name":{"type":"string"}},"required":["worker_name","cwd","provider","model","scope_summary","tools","context_tokens","context_window"],"type":"object"},"InFlightBlock":{"oneOf":[{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"thinking","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"type":"string"},"id":{"type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"state":{"$ref":"#/$defs/InFlightToolCallState"}},"required":["kind","id","name","args"],"type":"object"}]},"InFlightCompaction":{"additionalProperties":false,"properties":{"phase":{"$ref":"#/$defs/CompactionPhase"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"trigger":{"$ref":"#/$defs/CompactionTrigger"}},"required":["phase","started_at_ms","trigger"],"type":"object"},"InFlightSnapshot":{"additionalProperties":false,"description":"Unfinished model output and active command state included in\n`Event::Snapshot` for clients that attach while work is still streaming.\n\nThese blocks are presentation state only: they are reconstructed from the\nactive Worker controller and must not be treated as committed assistant\nhistory. Finalized assistant items continue to come from ordinary snapshot\nentries.","properties":{"blocks":{"items":{"$ref":"#/$defs/InFlightBlock"},"type":"array"},"commands":{"items":{"$ref":"#/$defs/CommandSnapshot"},"type":"array"},"compaction":{"anyOf":[{"$ref":"#/$defs/InFlightCompaction"},{"type":"null"}],"description":"The currently running compaction, if any.\n\nThis is lifecycle progress only. Candidate history and the staged\nSegment remain private until the Segment is activated atomically."}},"type":"object"},"InFlightToolCallState":{"enum":["pending","streaming_args","done"],"type":"string"},"InternalWorkerKind":{"description":"Presentation category for an Internal Worker exposed through its parent's\nprotocol stream. Internal Workers never become independently addressable\nprotocol subjects.","oneOf":[{"enum":["sub_worker"],"type":"string"},{"additionalProperties":false,"properties":{"service":{"additionalProperties":false,"properties":{"kind":{"type":"string"}},"required":["kind"],"type":"object"}},"required":["service"],"type":"object"}]},"InternalWorkerRef":{"additionalProperties":false,"description":"Stable presentation identity for one parent-owned Internal Worker session.\n`name` is display-only; `session_id` is the identity used by clients.","properties":{"kind":{"$ref":"#/$defs/InternalWorkerKind"},"name":{"type":"string"},"parent_session_id":{"type":["string","null"]},"session_id":{"type":"string"}},"required":["session_id","name","kind"],"type":"object"},"InternalWorkerSnapshot":{"additionalProperties":false,"description":"Reconnect state for one visible Internal Worker. The revision fences live\n`Event::InternalWorker` updates that raced with parent snapshot assembly.","properties":{"error":{"type":["string","null"]},"greeting":{"anyOf":[{"$ref":"#/$defs/Greeting"},{"type":"null"}],"description":"Public execution metadata for this child. Older snapshots and service\nworkers may omit it; clients must never substitute the parent's values."},"in_flight":{"$ref":"#/$defs/InFlightSnapshot"},"internal_workers":{"items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"session":{"$ref":"#/$defs/SessionSnapshot"},"status":{"$ref":"#/$defs/WorkerStatus","default":"idle"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","revision","session","status"],"type":"object"},"InvokeKind":{"description":"Kind of trigger that opened a new Invoke (IDLE → active) range.\n\nOne Invoke groups all entries from this trigger up to the next\n`Invoke` marker. The kind is the only payload — content (user text,\nnotify message, worker event body) is delivered by the immediately\nfollowing Turn entry, not by the marker itself.","oneOf":[{"const":"user_send","description":"`Method::Submit` — a user submission.","type":"string"},{"const":"notify","description":"`Method::Notify` — free-text notification injected into history.","type":"string"},{"const":"worker_event","description":"`Method::WorkerEvent` — typed lifecycle report from a child Worker.","type":"string"},{"const":"system_reminder","description":"A typed system reminder that crosses an IDLE boundary (mid-run\nreminders that don't break IDLE are SystemItem-only and do not\nopen a new Invoke).","type":"string"},{"const":"wakeup","description":"Cron / RemoteTrigger style scheduled wake-up. Reserved; no\nproducer is wired today but the variant is part of the initial\nset so future schedulers don't have to amend the wire enum.","type":"string"}]},"MemoryWorkerEvent":{"additionalProperties":false,"properties":{"message":{"description":"Human-readable compact form for actionbar rendering.","type":"string"},"reason":{"type":"string"},"run_id":{"type":"string"},"status":{"type":"string"},"timestamp_ms":{"description":"Milliseconds since the Unix epoch.","format":"int64","maximum":9007199254740991.0,"minimum":-9007199254740991.0,"type":"integer"},"trigger":{"type":"string"},"worker":{"type":"string"}},"required":["worker","status","run_id","trigger","reason","message","timestamp_ms"],"type":"object"},"Method":{"oneOf":[{"additionalProperties":false,"description":"Durably accept typed input for immediate activation or the session FIFO.\nRunning or Paused Workers queue Submit for a later turn; use Notify for\nadvisory information to incorporate into work already in progress.\n\n`submission_request_id` is generated by the authenticated caller and is\nused only for idempotent retry. Worker allocates the durable\n`submission_id` returned by [`Event::SubmissionAccepted`].","properties":{"method":{"const":"submit","type":"string"},"params":{"additionalProperties":false,"properties":{"input":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","input"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Human-readable text injected into the target Worker's LLM context as a\nnon-blocking system message. Accepted notifications are delivered at the\nnext between-turn boundary, or start a coalesced notification run while\nthe Worker is idle. No side effects beyond LLM context; use\n`WorkerEvent` for typed lifecycle reports.","properties":{"method":{"const":"notify","type":"string"},"params":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Typed lifecycle report from a child Worker to its direct parent.","properties":{"method":{"const":"worker_event","type":"string"},"params":{"$ref":"#/$defs/WorkerEvent"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Return the authoritative FIFO summary without exposing queued payloads.","properties":{"method":{"const":"list_pending_submissions","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Remove one queued submission. Running or already activated submissions\nare immutable and therefore cannot be cancelled here.","properties":{"method":{"const":"cancel_pending_submission","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"submission_id":{"type":"string"}},"required":["submission_id","expected_revision"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Remove every queued submission while preserving the active run.","properties":{"method":{"const":"clear_pending_submissions","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["expected_revision"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Activate the next queued submission while the Worker is idle. This is an\nexplicit recovery operation and never resumes a paused run implicitly.","properties":{"method":{"const":"continue_pending","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_head_id":{"type":"string"},"expected_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["expected_revision","expected_head_id"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"resume","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"cancel","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Stop the in-flight turn and transition to `Paused`.\n\nUnlike `Cancel` (which discards and returns to `Idle`), a paused\nWorker can resume the interrupted work via `Resume`, or accept a\nfresh `Submit` (orphan `tool_use` items are closed with a\nsynthetic tool result before the new user message is appended).","properties":{"method":{"const":"pause","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Request an explicit compaction while the Worker is otherwise idle.\n\nThis is a typed control method: clients must not send `compact` as a\n`Method::Submit` user message.","properties":{"method":{"const":"compact","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Ask the Worker to list valid rewind targets from its authoritative session log.","properties":{"method":{"const":"list_rewind_targets","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Truncate the current session back to the selected rewind target and\nreturn the selected user input to the client composer.","properties":{"method":{"const":"rewind_to","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"target":{"$ref":"#/$defs/RewindTargetId"}},"required":["target","expected_head_entries"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"shutdown","type":"string"},"params":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandEnvelope"}},"required":["command"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Request a list of completion candidates from the Worker.\n\nReply is sent on the same socket as `Event::Completions` (not\nbroadcast). The IPC server handles this directly and writes\nthe response straight back to the requesting socket.","properties":{"method":{"const":"list_completions","type":"string"},"params":{"additionalProperties":false,"properties":{"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"type":"string"}},"required":["kind","prefix"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"List Workers visible to this Worker from durable Worker state and the spawned-child\nregistry. This is not a host-wide Worker universe query.","properties":{"method":{"const":"list_workers","type":"string"}},"required":["method"],"type":"object"},{"additionalProperties":false,"description":"Restore a visible stopped/restorable Worker, or report that it is already\nlive. Missing state and not-visible state are distinct errors.","properties":{"method":{"const":"restore_worker","type":"string"},"params":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Register another existing Worker as a reciprocal peer of this Worker.\n\nThis is metadata/control state only: it does not ask the target's live\ncontroller for consent, and it must not grant delegated scope,\nspawned-child ownership, output cursors, or child lifecycle authority.","properties":{"method":{"const":"register_peer","type":"string"},"params":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"}},"required":["method","params"],"type":"object"}]},"PasteArtifactAvailability":{"enum":["available","unavailable","integrity_failed"],"type":"string"},"PasteArtifactMediaType":{"description":"One typed piece of a user submission.\n\n`Method::Submit` and `Event::UserMessage` carry `Vec`. Dumb\nclients (CLI piping, scripts) only need to produce a single\n`Segment::Text`; richer clients (TUI / GUI) construct typed atoms\n(paste chips, file refs) and\nsend them through directly so the Worker side never has to re-parse a\nflattened string.\n\nForward compat: payloads with unknown `kind` deserialize to\n`Segment::Unknown`. Worker treats this the same as known-but-unresolved\nvariants — emits an alert and inserts a `[unknown input segment]`\nplaceholder into the LLM context so neither user nor LLM is blind to\nthe dropped intent.","enum":["text_plain_utf8"],"type":"string"},"PasteArtifactRef":{"additionalProperties":false,"description":"Session-owned reference to a large pasted-input artifact.\n\nThe reference contains only bounded integrity and provenance metadata. The\nartifact body remains in session storage and is available to the model only\nthrough the scoped paste-artifact tools installed by Worker.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/PasteArtifactAvailability","description":"Availability observed when this immutable reference was committed.\nReads revalidate storage and integrity rather than trusting this field."},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"char_count":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"line_count":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"media_type":{"$ref":"#/$defs/PasteArtifactMediaType"},"sha256":{"type":"string"},"source_entry_id":{"type":"string"}},"required":["artifact_id","created_at_ms","media_type","availability","byte_len","char_count","line_count","sha256","source_entry_id"],"type":"object"},"PendingSubmissionSummary":{"additionalProperties":false,"description":"Bounded public projection of one pending submission. Only a short display\npreview is exposed; full payload segments and provenance remain in the log.","properties":{"accepted_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"preview":{"description":"Single-line display text, at most 240 Unicode scalars plus an ellipsis.\nAbsent on older runtimes. Never used to reconstruct the submitted input.","type":["string","null"]},"segment_count":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"submission_id":{"type":"string"}},"required":["submission_id","accepted_at_ms","segment_count","byte_len"],"type":"object"},"PendingSubmissionsSnapshot":{"additionalProperties":false,"description":"Revisioned session-owned FIFO projection used by snapshots and live events.","properties":{"head_id":{"default":null,"type":["string","null"]},"notification_count":{"default":0,"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"notification_previews":{"description":"Ordered display-only previews of waiting notifications, each bounded like\nPendingSubmissionSummary::preview. Older runtimes omit this field.","items":{"type":"string"},"type":"array"},"revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"submissions":{"default":[],"items":{"$ref":"#/$defs/PendingSubmissionSummary"},"type":"array"}},"required":["revision","head_id","notification_count","submissions"],"type":"object"},"Permission":{"description":"Permission lattice used by [`ScopeRule`].\n\nThe derived `Ord` instance follows declaration order, so\n`Read < Write`. Allow rules grant the stated level (and by extension\neverything below); deny rules cap the effective level **strictly\nbelow** the stated level.","enum":["read","write"],"type":"string"},"ReasoningConfig":{"description":"Public reasoning configuration resolved for the model client.\n\nThis deliberately exposes only the effective provider control, not the\nWorker manifest or provider credentials. The tagged shape prevents token\nbudgets from being presented as effort labels.","oneOf":[{"additionalProperties":false,"properties":{"effort":{"type":"string"},"kind":{"const":"effort","type":"string"}},"required":["kind","effort"],"type":"object"},{"additionalProperties":false,"properties":{"budget_tokens":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"},"kind":{"const":"budget_tokens","type":"string"}},"required":["kind","budget_tokens"],"type":"object"}]},"RewindSummary":{"additionalProperties":false,"properties":{"discarded_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_side_effect_warning":{"type":"boolean"},"truncated_to_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["truncated_to_entries","discarded_entries","tool_side_effect_warning"],"type":"object"},"RewindTarget":{"additionalProperties":false,"properties":{"disabled_reason":{"type":["string","null"]},"eligible":{"type":"boolean"},"expected_head_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"id":{"$ref":"#/$defs/RewindTargetId"},"preview":{"type":"string"},"timestamp_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]},"truncate_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"turn_index":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"warning":{"type":["string","null"]}},"required":["id","expected_head_entries","truncate_entries","turn_index","preview","eligible"],"type":"object"},"RewindTargetId":{"additionalProperties":false,"properties":{"segment_id":{"type":"string"},"user_input_entry_index":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["segment_id","user_input_entry_index"],"type":"object"},"RunFailureKind":{"enum":["engine","compaction"],"type":"string"},"RunResult":{"oneOf":[{"enum":["finished","paused","limit_reached"],"type":"string"},{"const":"cancelled","description":"The logical Run ended because the user intentionally cancelled it after\nobservable work. Unlike `RolledBack`, committed history is retained.","type":"string"},{"const":"rolled_back","description":"The accepted Method::Submit produced no assistant/tool output before\nuser interruption, so the Worker rolled the submit-time turn state back\nto its pre-submit snapshot. Clients should treat the Worker as Idle and\nrestore the just-submitted input into the editable composer if desired.","type":"string"}]},"RunResumeSource":{"enum":["compaction","pause"],"type":"string"},"RunYieldReason":{"enum":["compaction"],"type":"string"},"ScopeRule":{"additionalProperties":false,"description":"A single allow or deny rule inside a scope configuration.","properties":{"permission":{"$ref":"#/$defs/Permission","description":"Permission level this rule grants (allow) or caps strictly below\n(deny)."},"recursive":{"default":true,"description":"When `false`, the rule only matches the target itself and its\ndirect children. Defaults to `true`.","type":"boolean"},"symlink_policy":{"$ref":"#/$defs/SymlinkPolicy","default":"resolved","description":"Which path identity an allow rule uses when symbolic links are\nencountered. Deny rules always inspect both identities."},"target":{"description":"Target path. Must be absolute by the time a `Scope` is built from\nthis rule — relative paths are resolved per-layer against the\nmanifest file's directory (cwd for overlay layers) before cascade\nmerge.","type":"string"}},"required":["target","permission","recursive","symlink_policy"],"type":"object"},"Segment":{"oneOf":[{"additionalProperties":false,"description":"Free-form text. The fallback every client can produce.","properties":{"content":{"type":"string"},"kind":{"const":"text","type":"string"}},"required":["kind","content"],"type":"object"},{"additionalProperties":false,"description":"Bracketed-paste capture from a TUI-style client. `id`, `chars`\nand `lines` carry the metadata needed to re-render a\n`[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage`\nre-broadcast.","properties":{"chars":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"content":{"type":"string"},"id":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"kind":{"const":"paste","type":"string"},"lines":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"}},"required":["kind","id","chars","lines","content"],"type":"object"},{"additionalProperties":false,"description":"Internal reference produced when Worker stores a large `Paste` before\ncommitting input. Clients may receive this in history/event projections;\nthe body is intentionally absent.","properties":{"artifact":{"$ref":"#/$defs/PasteArtifactRef"},"kind":{"const":"paste_artifact","type":"string"}},"required":["kind","artifact"],"type":"object"},{"additionalProperties":false,"description":"Client-local file uploaded into the owning Worker session before submit.\nThe Session Log stores only this immutable reference, never file bytes or\nthe client's local path.","properties":{"file":{"$ref":"#/$defs/UploadedFileRef"},"kind":{"const":"uploaded_file","type":"string"}},"required":["kind","file"],"type":"object"},{"additionalProperties":false,"description":"`@` file-system reference. Worker resolves readable files to\n`[File: ]` attachments and readable normal directories to shallow\n`[Dir: ]` listings; the flattened user text keeps the literal\n`@` placeholder either way.","properties":{"kind":{"const":"file_ref","type":"string"},"path":{"type":"string"}},"required":["kind","path"],"type":"object"},{"additionalProperties":false,"description":"Source-qualified request for the host to start a Flow before committing\nthis user input. Runtime resolves it through Workspace authority and\nreplaces it with the entered state's instructions before Worker-side\ninput resolution.","properties":{"kind":{"const":"flow","type":"string"},"selector":{"type":"string"}},"required":["kind","selector"],"type":"object"},{"additionalProperties":false,"description":"Unknown variant from a newer client. Worker treats this as an\nunresolved input — surfaces an alert and inserts a placeholder.\nRound-trip is lossy: re-serializing yields `{\"kind\":\"unknown\"}`.","properties":{"kind":{"const":"unknown","type":"string"}},"required":["kind"],"type":"object"}]},"SessionContentPart":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"refusal","type":"string"},"refusal":{"type":"string"}},"required":["kind","refusal"],"type":"object"}]},"SessionEntryProvenance":{"enum":["human_input","worker_input","flow_instruction","backend_instruction","model_output","tool_output","derived_summary","legacy_unknown"],"type":"string"},"SessionMessageRole":{"enum":["user","assistant"],"type":"string"},"SessionSnapshot":{"additionalProperties":false,"description":"Canonical, storage-independent projection of committed session history.\n\nWorker protocols expose this DTO instead of append-log records. New\nstorage variants can therefore be added without teaching every client how\nto replay the durable log format.","properties":{"entries":{"items":{"$ref":"#/$defs/SessionSnapshotEntry"},"type":"array"},"pending_submissions":{"$ref":"#/$defs/PendingSubmissionsSnapshot","default":{"head_id":null,"notification_count":0,"revision":0,"submissions":[]}}},"required":["entries","pending_submissions"],"type":"object"},"SessionSnapshotEntry":{"oneOf":[{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"user_input","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","segments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"items":{"$ref":"#/$defs/SessionContentPart"},"type":"array"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"message","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"role":{"$ref":"#/$defs/SessionMessageRole"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","role","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"call_id":{"type":"string"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","name","arguments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"attachments":{"items":{"$ref":"#/$defs/SessionToolAttachment"},"type":"array"},"call_id":{"type":"string"},"content":{"type":["string","null"]},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"is_error":{"type":"boolean"},"kind":{"const":"tool_result","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"summary":{"type":"string"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","summary","is_error","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"type":"string"},"data":true,"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"item_kind":{"type":"string"},"kind":{"const":"system_item","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","item_kind","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable, non-terminal control return from one logical Run. This is not a\ncompleted run and must not be used to infer the current Worker state.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_yielded","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"reason":{"$ref":"#/$defs/RunYieldReason"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","reason","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable boundary immediately before Engine execution resumes. The source\ndistinguishes automatic compaction continuation from an intentional\nuser-pause resume; it does not prove that later model output occurred.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_resumed","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"source":{"$ref":"#/$defs/RunResumeSource"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","source","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable terminal for an intentional cancellation. Consumers must not\npresent this lifecycle result as an execution failure.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_cancelled","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"failure":{"anyOf":[{"$ref":"#/$defs/RunFailureKind"},{"type":"null"}]},"kind":{"const":"run_error","type":"string"},"message":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","message","entry_id","timestamp","provenance"],"type":"object"}]},"SessionToolAttachment":{"additionalProperties":false,"properties":{"attachment_id":{"description":"Stable, bounded identity derived from the owning Session entry, attachment\nposition, and immutable image bytes. It is a locator, not a bearer secret.","type":"string"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"media_type":{"type":"string"}},"required":["attachment_id","media_type","byte_len"],"type":"object"},"SubmissionDisposition":{"description":"Durable acceptance result for one idempotent Submit request.","oneOf":[{"enum":["started"],"type":"string"},{"const":"queued","description":"Input is durably held in the session FIFO until eligible for activation.","type":"string"}]},"SubscriptionEvent":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"payload":{"$ref":"#/$defs/SubscriptionEventPayload"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","payload"],"type":"object"},"event":{"const":"event","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionTerminationCode"},"message":{"type":"string"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","code","message"],"type":"object"},"event":{"const":"subscription_closed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionEventPayload":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"worker":{"$ref":"#/$defs/SubscriptionWorker"}},"required":["worker"],"type":"object"},"event":{"const":"worker_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id"],"type":"object"},"event":{"const":"worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","event"],"type":"object"},"event":{"const":"worker_protocol","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdir":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"}},"required":["workdir"],"type":"object"},"event":{"const":"workdir_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id"],"type":"object"},"event":{"const":"workdir_removed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionId":{"type":"string"},"SubscriptionRejectionCode":{"enum":["invalid_request","unsupported_protocol_version","unsupported_selector","unauthorized","resource_not_found","capacity_exceeded","internal"],"type":"string"},"SubscriptionRequest":{"oneOf":[{"additionalProperties":false,"properties":{"method":{"const":"subscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"}},"required":["request_id","selector"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"unsubscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"}},"required":["method","params"],"type":"object"}]},"SubscriptionRequestId":{"type":"string"},"SubscriptionResponse":{"oneOf":[{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"},"snapshot":{"$ref":"#/$defs/SubscriptionSnapshot"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id","selector","snapshot"],"type":"object"},"result":{"const":"subscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"},"result":{"const":"unsubscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionRejectionCode"},"message":{"type":"string"},"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"anyOf":[{"$ref":"#/$defs/SubscriptionId"},{"type":"null"}]}},"required":["request_id","code","message"],"type":"object"},"result":{"const":"subscription_rejected","type":"string"}},"required":["result","payload"],"type":"object"}]},"SubscriptionSnapshot":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workers":{"items":{"$ref":"#/$defs/SubscriptionWorker"},"type":"array"}},"required":["workers"],"type":"object"},"topic":{"const":"workers","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"events":{"items":{"$ref":"#/$defs/Event"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","events"],"type":"object"},"topic":{"const":"worker_protocol","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdirs":{"items":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"},"type":"array"}},"required":["workdirs"],"type":"object"},"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic","data"],"type":"object"}]},"SubscriptionTerminationCode":{"enum":["lagged","resource_gone","unauthorized","server_shutdown"],"type":"string"},"SubscriptionWorkdirId":{"type":"string"},"SubscriptionWorker":{"additionalProperties":false,"properties":{"availability":{"$ref":"#/$defs/SubscriptionWorkerAvailability","default":"observed","description":"Freshness of the Runtime-backed observation carried by this projection.\n`Unavailable` preserves catalog membership without claiming that execution stopped."},"display_name":{"type":["string","null"]},"has_running_internal_workers":{"default":false,"type":"boolean"},"job":{"anyOf":[{"$ref":"#/$defs/SubscriptionWorkerJob"},{"type":"null"}],"description":"Backend-owned Job binding. Runtime producers leave this unset; the\nWorkspace projection enriches both snapshots and live updates."},"profile":{"type":["string","null"]},"resource_key":{"description":"Workspace-scoped canonical resource key. Runtime producers leave this unset;\nWorkspace-facing projections must populate it before publishing the Worker.","type":["string","null"]},"runtime_id":{"description":"Set by the Workspace Server when projecting a Runtime-owned Worker to clients.\nRuntime producers leave this unset because the connection identifies the Runtime.","type":["string","null"]},"state":{"$ref":"#/$defs/SubscriptionWorkerState","description":"Runtime catalog lifecycle compatibility projection; not foreground-state authority."},"workdir_attachments":{"items":{"$ref":"#/$defs/SubscriptionWorkerWorkdirAttachment"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"},"worker_state":{"anyOf":[{"$ref":"#/$defs/WorkerStateSnapshot"},{"type":"null"}],"description":"Latest foreground state observed from the Worker. This remains\nabsent until an authoritative Worker snapshot/event has been applied."},"workspace_id":{"type":["string","null"]}},"required":["worker_id","state","availability","has_running_internal_workers"],"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"},"SubscriptionWorkerId":{"type":"string"},"SubscriptionWorkerIds":{"description":"A bounded, canonical worker-id set. Ordering on the wire and in equality/hash\nsemantics is lexical, so equivalent selectors aggregate to one upstream key.","items":{"$ref":"#/$defs/SubscriptionWorkerId"},"type":"array"},"SubscriptionWorkerJob":{"additionalProperties":false,"properties":{"attempt_id":{"type":"string"},"job_id":{"type":"string"},"purpose":{"type":"string"}},"required":["job_id","attempt_id","purpose"],"type":"object"},"SubscriptionWorkerProtocolMethod":{"additionalProperties":false,"properties":{"method":{"$ref":"#/$defs/Method"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","method"],"type":"object"},"SubscriptionWorkerState":{"enum":["idle","running","paused","stopped"],"type":"string"},"SubscriptionWorkerWorkdirAttachment":{"additionalProperties":false,"properties":{"alias":{"description":"Stable Worker-local routing alias.","type":"string"},"repository_key":{"type":["string","null"]},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["alias","working_directory_id"],"type":"object"},"SymlinkPolicy":{"description":"Symbolic-link identity used by one filesystem allow rule.\n\n`Resolved` is the least authority and the default: access is matched\nagainst the provider-resolved target. `Logical` intentionally grants the\npath as presented through the Workdir, even when it aliases another target.","enum":["resolved","logical"],"type":"string"},"ToolResultDisposition":{"enum":["success","error","interrupted","cancelled","outcome_unknown"],"type":"string"},"TurnResult":{"enum":["finished","paused"],"type":"string"},"UploadedFileAvailability":{"description":"Availability recorded for an uploaded client-local file.","enum":["available","unavailable","integrity_failed"],"type":"string"},"UploadedFileRef":{"additionalProperties":false,"description":"Session-owned immutable reference to a client-local uploaded file.\n\nUpload transports return an unbound reference. Worker fills\n`source_entry_id` immediately before the containing user input is committed;\ncommitted Session Log and public snapshot records therefore always retain\nthe durable source-entry identity without storing the file body.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/UploadedFileAvailability"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"file_name":{"type":"string"},"media_type":{"type":"string"},"sha256":{"type":"string"},"source_entry_id":{"type":["string","null"]}},"required":["artifact_id","file_name","media_type","created_at_ms","availability","byte_len","sha256"],"type":"object"},"WorkerBusyState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"run","type":"string"},"state":{"$ref":"#/$defs/WorkerRunState"}},"required":["kind","state"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"maintenance","type":"string"},"state":{"$ref":"#/$defs/WorkerMaintenanceState"}},"required":["kind","state"],"type":"object"}]},"WorkerCommandAcknowledgement":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandKind"},"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"disposition":{"$ref":"#/$defs/WorkerCommandDisposition"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"The complete authoritative state observed after command admission."}},"required":["command_id","command","disposition","state"],"type":"object"},"WorkerCommandDisposition":{"enum":["accepted","stale_command_id","conflict","invalid_state"],"type":"string"},"WorkerCommandEnvelope":{"additionalProperties":false,"description":"Caller-owned identity for one state-changing Worker command.\n\nA controller accepts command ids in strictly increasing order. Exact retries\nof an accepted id must retain the same command kind.","properties":{"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["command_id"],"type":"object"},"WorkerCommandKind":{"enum":["resume","cancel","pause","compact","shutdown"],"type":"string"},"WorkerEvent":{"description":"Typed lifecycle events sent from a child Worker to its parent.\n\nDelivered as `Method::WorkerEvent` over the parent's Unix socket. The\nparent Controller always applies variant-specific side effects\n(registry / worker-allocation updates). Agent-visible variants are also\nqueued into the notification buffer; control-plane-only variants are\nnot injected into the parent's LLM context.\n\nTransport is fire-and-forget; receivers must tolerate out-of-order\ndelivery (e.g. `TurnEnded` arriving after `ShutDown` for the same\nchild Worker).","oneOf":[{"additionalProperties":false,"description":"Child finished one turn and is back to IDLE.","properties":{"kind":{"const":"turn_ended","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Engine execution error occurred inside the child's turn.\n\nLimited to worker runtime failures (provider / tool errors) —\ndoes not include transient method-rejection responses such as\n`AlreadyRunning`.","properties":{"kind":{"const":"errored","type":"string"},"message":{"type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name","message"],"type":"object"},{"additionalProperties":false,"description":"Child has stopped (controller loop is exiting).","properties":{"kind":{"const":"shut_down","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Child SubWorker sub-delegated scope to a grandchild SubWorker via `SubWorkerSpawn`.\n\nControl-plane only: receivers apply registry side effects and\npropagate upward, but do not expose this as an agent notification.\n\nThe parent uses this to add the grandchild to its own\n`spawned_workers.json` so it can manage the grandchild directly\neven if the intermediate child dies. The parent then re-fires\nthis event upward (if it has a parent of its own) to maintain\nthe chain to root.","properties":{"kind":{"const":"scope_sub_delegated","type":"string"},"parent_worker":{"description":"Sub-delegating Worker (= the sender itself).","type":"string"},"scope":{"description":"Scope delegated to the grandchild.","items":{"$ref":"#/$defs/ScopeRule"},"type":"array"},"sub_socket":{"description":"Unix-socket path where the grandchild is reachable.","type":"string"},"sub_worker":{"description":"Name of the grandchild Worker.","type":"string"}},"required":["kind","parent_worker","sub_worker","sub_socket","scope"],"type":"object"}]},"WorkerMaintenanceState":{"enum":["compacting"],"type":"string"},"WorkerRunState":{"enum":["running","pausing","paused","cancelling"],"type":"string"},"WorkerState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"idle","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"busy","type":"string"},"state":{"$ref":"#/$defs/WorkerBusyState"}},"required":["kind","state"],"type":"object"}]},"WorkerStateSnapshot":{"additionalProperties":false,"properties":{"last_command_id":{"description":"Highest lifecycle command id observed by this controller instance.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"last_finished_submission_request_id":{"description":"Most recent Submit request whose controller Run has returned and whose\npost-Run state transition has been published. This is a teardown fence,\nnot a successful-result claim; cancelled and failed Runs may also finish.","type":["string","null"]},"state":{"$ref":"#/$defs/WorkerState"}},"required":["last_command_id","state"],"type":"object"},"WorkerStatus":{"enum":["idle","running","paused","stopped"],"type":"string"},"WorkspaceSubscriptionWorkdir":{"additionalProperties":false,"description":"Workspace-facing Workdir summary. Backend-generated Repository UUIDs never\nenter this DTO; Workspace Server must resolve the required Repository key.","properties":{"repository_key":{"type":"string"},"state":{"type":"string"},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id","repository_key","state"],"type":"object"}},"$schema":"https://json-schema.org/draft/2020-12/schema","description":"Direction is explicit at the outer envelope, while concrete message kinds\nremain typed within each lane.","oneOf":[{"additionalProperties":false,"properties":{"frame":{"const":"request","type":"string"},"message":{"$ref":"#/$defs/SubscriptionRequest"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"response","type":"string"},"message":{"$ref":"#/$defs/SubscriptionResponse"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"event","type":"string"},"message":{"$ref":"#/$defs/SubscriptionEvent"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"worker_protocol","type":"string"},"message":{"$ref":"#/$defs/SubscriptionWorkerProtocolMethod"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"}],"title":"SubscriptionFrame"}; export const RUST_SERIALIZED_SUBSCRIPTION_FRAME_FIXTURES: readonly unknown[] = [{"protocol_version":2,"frame":"response","message":{"result":"subscribed","payload":{"request_id":"fixture-request","subscription_id":"fixture-subscription","selector":{"topic":"workspace_workers"},"snapshot":{"topic":"workers","data":{"workers":[{"worker_id":"fixture-worker","runtime_id":"fixture-runtime","resource_key":"fixture-worker-resource","availability":"observed","state":"idle","has_running_internal_workers":false,"workspace_id":"fixture-workspace","workdir_attachments":[{"alias":"external","working_directory_id":"fixture-external-workdir"}]}]}}}}},{"protocol_version":2,"frame":"event","message":{"event":"event","data":{"subscription_id":"fixture-subscription","payload":{"event":"worker_protocol","data":{"worker_id":"fixture-worker","event":{"event":"pending_submissions_changed","data":{"pending":{"revision":0,"notification_count":0,"head_id":null,"submissions":[]}}}}}}}},{"protocol_version":2,"frame":"event","message":{"event":"event","data":{"subscription_id":"fixture-subscription","payload":{"event":"worker_protocol","data":{"worker_id":"fixture-worker","event":{"event":"snapshot","data":{"session":{"pending_submissions":{"revision":0,"notification_count":0,"head_id":null,"submissions":[]},"entries":[]},"greeting":{"worker_name":"fixture-worker","cwd":"/fixture","provider":"openai_responses","model":"gpt-6-astra","reasoning":{"kind":"effort","effort":"high"},"scope_summary":"fixture scope","tools":["Read"],"context_window":272000,"context_tokens":142000,"context_usage":{"tokens":142000,"source":"measured"}},"state":{"last_command_id":0,"state":{"kind":"idle"}},"internal_workers":[{"worker":{"session_id":"fixture-child-session","name":"fixture-child","parent_session_id":"fixture-parent-session","kind":"sub_worker"},"revision":1,"session":{"pending_submissions":{"revision":0,"notification_count":0,"head_id":null,"submissions":[]},"entries":[]},"greeting":{"worker_name":"fixture-worker","cwd":"/fixture","provider":"openai_responses","model":"gpt-6-astra","reasoning":{"kind":"effort","effort":"high"},"scope_summary":"fixture scope","tools":["Read"],"context_window":272000,"context_tokens":142000,"context_usage":{"tokens":142000,"source":"measured"}},"status":"idle"}]}}}}}}}]; const utf8 = new TextEncoder(); diff --git a/web/workspace/src/lib/generated/protocol.ts b/web/workspace/src/lib/generated/protocol.ts index 902067ae6..94e91de69 100644 --- a/web/workspace/src/lib/generated/protocol.ts +++ b/web/workspace/src/lib/generated/protocol.ts @@ -34,7 +34,13 @@ export type WorkerStateSnapshot = { /** * Highest lifecycle command id observed by this controller instance. */ -last_command_id: number, state: WorkerState, }; +last_command_id: number, +/** + * Most recent Submit request whose controller Run has returned and whose + * post-Run state transition has been published. This is a teardown fence, + * not a successful-result claim; cancelled and failed Runs may also finish. + */ +last_finished_submission_request_id?: string | null, state: WorkerState, }; export type TurnResult = "finished" | "paused"; @@ -129,12 +135,12 @@ export type SessionMessageRole = "user" | "assistant"; export type SessionContentPart = { "kind": "text", text: string, } | { "kind": "refusal", refusal: string, }; -export type SessionToolAttachment = { media_type: string, +export type SessionToolAttachment = { /** - * Base64-encoded durable attachment body. Public snapshots preserve the - * committed multimodal value instead of replacing it with placeholder text. + * Stable, bounded identity derived from the owning Session entry, attachment + * position, and immutable image bytes. It is a locator, not a bearer secret. */ -data_base64: string, }; +attachment_id: string, media_type: string, byte_len: number, }; export type RunYieldReason = "compaction"; diff --git a/web/workspace/src/lib/generated/repository-api.ts b/web/workspace/src/lib/generated/repository-api.ts index ee61147a4..052d8f330 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:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098) +// Input contract: 0.1.0 (sha256:f9f0f8918dbcce386e94d5368fa938f02565ca98f9f67f07537bd88ed8c49870) // 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 @@ -69,7 +69,7 @@ export type WorkerRunState = "running" | "pausing" | "paused" | "cancelling"; export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyState; }; -export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; }; +export type WorkerStateSnapshot = { last_command_id: number; last_finished_submission_request_id?: string | null; state: WorkerState; }; export type WorkerSummary = { availability: SubscriptionWorkerAvailability; 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; }; diff --git a/web/workspace/src/lib/generated/runtime-api.ts b/web/workspace/src/lib/generated/runtime-api.ts index 905b323d2..2a3b02082 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:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098) +// Input contract: 0.1.0 (sha256:f9f0f8918dbcce386e94d5368fa938f02565ca98f9f67f07537bd88ed8c49870) // 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 @@ -159,7 +159,7 @@ export type WorkerRunState = "running" | "pausing" | "paused" | "cancelling"; export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyState; }; -export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; }; +export type WorkerStateSnapshot = { last_command_id: number; last_finished_submission_request_id?: string | null; state: WorkerState; }; export type WorkerSummary = { availability: SubscriptionWorkerAvailability; 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; }; diff --git a/web/workspace/src/lib/generated/ticket-api.ts b/web/workspace/src/lib/generated/ticket-api.ts index fbac54057..24d0a4a79 100644 --- a/web/workspace/src/lib/generated/ticket-api.ts +++ b/web/workspace/src/lib/generated/ticket-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098) +// Input contract: 0.1.0 (sha256:f9f0f8918dbcce386e94d5368fa938f02565ca98f9f67f07537bd88ed8c49870) // Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/ticket-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check diff --git a/web/workspace/src/lib/generated/workdir-api.ts b/web/workspace/src/lib/generated/workdir-api.ts index 97b8c9c7c..0fa79bece 100644 --- a/web/workspace/src/lib/generated/workdir-api.ts +++ b/web/workspace/src/lib/generated/workdir-api.ts @@ -1,6 +1,6 @@ // @generated by yoi-workdir-openapi-typescript@1. Do not edit. // Input: openapi/server-api.json -// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098) +// Input contract: 0.1.0 (sha256:f9f0f8918dbcce386e94d5368fa938f02565ca98f9f67f07537bd88ed8c49870) // Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output // Output: web/workspace/src/lib/generated/workdir-api.ts // Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check diff --git a/web/workspace/src/lib/generated/worker-launch-api.ts b/web/workspace/src/lib/generated/worker-launch-api.ts index ff2df6358..42881f6ce 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:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098) +// Input contract: 0.1.0 (sha256:f9f0f8918dbcce386e94d5368fa938f02565ca98f9f67f07537bd88ed8c49870) // 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 @@ -11,7 +11,7 @@ export type BrowserWorkerWorkingDirectorySelection = { alias: string; relative_c export type BrowserWorkspaceOrchestratorResponse = { diagnostics: Array; disposition: string; online: boolean; worker?: WorkerLaunchWorkerSummary | null; workspace_id: string; }; -export type CreateWorkspaceWorkerRequest = { control_operation_id?: string | null; display_name: string; initial_submit?: Array; profile?: string | null; runtime_id: string; singleton_key?: string | null; ticket_assignment?: CreateWorkspaceWorkerTicketAssignmentRequest | null; workdir_attachments?: Array; }; +export type CreateWorkspaceWorkerRequest = { control_operation_id?: string | null; display_name: string; feature_connections: WorkspaceWorkerFeatureConnectionsRequest; initial_submit?: Array; profile?: string | null; runtime_id: string; singleton_key?: string | null; ticket_assignment?: CreateWorkspaceWorkerTicketAssignmentRequest | null; workdir_attachments?: Array; }; export type CreateWorkspaceWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; }; @@ -37,6 +37,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 SubjektivWorkerConnectionRequest = { subject_id: string; }; + export type SubscriptionWorkerAvailability = "observed" | "unavailable"; export type UploadedFileAvailability = "available" | "unavailable" | "integrity_failed"; @@ -47,9 +49,11 @@ export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: export type WorkerImplementationSummary = { display_hint: string; kind: string; }; +export type WorkerLaunchFeatureConnectionOptions = { subjektiv: boolean; }; + export type WorkerLaunchOptionsResponse = { default_profile?: string | null; diagnostics: Array; profiles: Array; repositories: Array; runtimes: Array; working_directories: Array; workspace_id: string; }; -export type WorkerLaunchProfileCandidate = { description: string; id: string; label: string; }; +export type WorkerLaunchProfileCandidate = { description: string; feature_connections: WorkerLaunchFeatureConnectionOptions; id: string; label: string; }; export type WorkerLaunchRuntimeOption = { built_in: boolean; diagnostics: Array; display_name: string; runtime_id: string; status: string; worker_creation_available: boolean; working_directory_required: boolean; }; @@ -63,7 +67,7 @@ export type WorkerRunState = "running" | "pausing" | "paused" | "cancelling"; export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyState; }; -export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; }; +export type WorkerStateSnapshot = { last_command_id: number; last_finished_submission_request_id?: string | null; state: WorkerState; }; export type WorkerSummary = { availability: SubscriptionWorkerAvailability; 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; }; @@ -84,3 +88,5 @@ export type WorkingDirectorySource = { kind: "repository"; repository_key: strin export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown"; export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; }; + +export type WorkspaceWorkerFeatureConnectionsRequest = { subjektiv?: SubjektivWorkerConnectionRequest | null; }; diff --git a/web/workspace/src/lib/workspace/api/workers.test.ts b/web/workspace/src/lib/workspace/api/workers.test.ts index c4ca9bdd3..f76b0b404 100644 --- a/web/workspace/src/lib/workspace/api/workers.test.ts +++ b/web/workspace/src/lib/workspace/api/workers.test.ts @@ -166,7 +166,12 @@ Deno.test("Worker launch options parser accepts the generated wire shape", () => diagnostics: [], }], default_profile: null, - profiles: [{ id: "builtin:coder", label: "Coder", description: "Code" }], + profiles: [{ + id: "builtin:coder", + label: "Coder", + description: "Code", + feature_connections: { subjektiv: true }, + }], repositories: [{ repository_key: "main" }], working_directories: [], diagnostics: [], @@ -235,10 +240,27 @@ Deno.test("Worker create request parser requires the complete shared request", ( working_directory_id: "workdir-a", relative_cwd: null, }], + feature_connections: {}, control_operation_id: null, }; assertEquals(parseCreateWorkspaceWorkerRequest(request), request); + assertEquals( + parseCreateWorkspaceWorkerRequest({ + ...request, + feature_connections: { subjektiv: { subject_id: "subject-42" } }, + }).feature_connections, + { subjektiv: { subject_id: "subject-42" } }, + ); + assertThrows( + () => + parseCreateWorkspaceWorkerRequest({ + ...request, + resolved_subjektiv_attached: true, + }), + Error, + "unknown field resolved_subjektiv_attached", + ); assertThrows( () => parseCreateWorkspaceWorkerRequest({ diff --git a/web/workspace/src/lib/workspace/api/workers.ts b/web/workspace/src/lib/workspace/api/workers.ts index c31a50c44..71c76c9f3 100644 --- a/web/workspace/src/lib/workspace/api/workers.ts +++ b/web/workspace/src/lib/workspace/api/workers.ts @@ -21,6 +21,7 @@ import type { WorkerWorkdirAttachmentSummary, WorkerWorkspaceSummary, WorkingDirectoryRepositoryOption, + WorkspaceWorkerFeatureConnectionsRequest, } from "#lib/generated/worker-launch-api.ts"; import type { Segment } from "#lib/generated/protocol.ts"; import { @@ -148,16 +149,26 @@ function runtimeOption( }; } +function featureConnectionOptions(value: unknown, label: string) { + const item = record(value, label); + exact(item, ["subjektiv"], label); + return { subjektiv: boolean(item.subjektiv, `${label}.subjektiv`) }; +} + function profileCandidate( value: unknown, label: string, ): WorkerLaunchProfileCandidate { const item = record(value, label); - exact(item, ["id", "label", "description"], label); + exact(item, ["id", "label", "description", "feature_connections"], label); return { id: string(item.id, `${label}.id`), label: string(item.label, `${label}.label`), description: string(item.description, `${label}.description`), + feature_connections: featureConnectionOptions( + item.feature_connections, + `${label}.feature_connections`, + ), }; } @@ -700,6 +711,20 @@ function ticketAssignment( }; } +function featureConnectionsRequest( + value: unknown, + label: string, +): WorkspaceWorkerFeatureConnectionsRequest { + const item = record(value, label); + exact(item, ["subjektiv"], label); + const subjektiv = optional(item.subjektiv, `${label}.subjektiv`, (value, itemLabel) => { + const connection = record(value, itemLabel); + exact(connection, ["subject_id"], itemLabel); + return { subject_id: string(connection.subject_id, `${itemLabel}.subject_id`) }; + }); + return subjektiv === undefined ? {} : { subjektiv }; +} + function unsignedInteger(value: unknown, label: string): number { const parsed = number(value, label); if (!Number.isSafeInteger(parsed) || parsed < 0) { @@ -853,6 +878,7 @@ export function parseCreateWorkspaceWorkerRequest( "ticket_assignment", "initial_submit", "workdir_attachments", + "feature_connections", "control_operation_id", ], "Worker create request", @@ -870,6 +896,10 @@ export function parseCreateWorkspaceWorkerRequest( "workdir_attachments", workingDirectorySelection, ), + feature_connections: featureConnectionsRequest( + item.feature_connections, + "feature_connections", + ), control_operation_id: nullableString( item.control_operation_id, "control_operation_id", diff --git a/web/workspace/src/lib/workspace/console/ConsoleAttachmentImage.svelte b/web/workspace/src/lib/workspace/console/ConsoleAttachmentImage.svelte new file mode 100644 index 000000000..83f7bef54 --- /dev/null +++ b/web/workspace/src/lib/workspace/console/ConsoleAttachmentImage.svelte @@ -0,0 +1,97 @@ + + +
+ {#if status === "ready" && objectUrl} + Tool output attachment + {:else} + {statusText()} + {#if url && ["missing", "expired", "unauthorized", "network", "failed"].includes(status)} + + {/if} + {/if} +
+ + diff --git a/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte b/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte index a5866a57e..229b504e3 100644 --- a/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte +++ b/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte @@ -1,13 +1,16 @@