From ca838e4acfcad3939f8681bcfcd1b077902b8898 Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 1 Oct 2026 10:06:55 +0900 Subject: [PATCH] feat: add durable backend job runner --- crates/manifest/src/builtin_profile.rs | 7 + crates/protocol/src/subscription.rs | 26 + crates/protocol/src/typescript.rs | 4 +- crates/worker-runtime/src/runtime.rs | 1 + crates/worker/src/controller.rs | 14 + crates/worker/src/feature/builtin.rs | 1 + .../src/feature/builtin/backend_job_result.rs | 175 +++ crates/worker/src/prompt/catalog.rs | 3 + crates/workspace-server/src/backend_job.rs | 489 +++++++ crates/workspace-server/src/hosts.rs | 6 + crates/workspace-server/src/latest_schema.sql | 73 ++ crates/workspace-server/src/lib.rs | 1 + crates/workspace-server/src/server.rs | 733 ++++++++++- crates/workspace-server/src/store.rs | 1128 ++++++++++++++++- .../src/workspace_deletion.rs | 3 + .../src/workspace_subscription.rs | 9 + docs/design/durable-operations.md | 49 + resources/profiles/backend-job.dcdl | 33 + resources/prompts/catalog.dcdl | 2 + .../prompts/internal/backend_job_system.md | 5 + .../src/lib/generated/protocol-validator.ts | 2 +- web/workspace/src/lib/generated/protocol.ts | 9 +- .../src/lib/workspace/sidebar/types.ts | 2 + .../sidebar/worker-subscription.test.ts | 41 + .../workspace/sidebar/worker-subscription.ts | 1 + .../src/lib/workspace/sidebar/workers.test.ts | 13 + .../src/lib/workspace/sidebar/workers.ts | 6 +- 27 files changed, 2823 insertions(+), 13 deletions(-) create mode 100644 crates/worker/src/feature/builtin/backend_job_result.rs create mode 100644 crates/workspace-server/src/backend_job.rs create mode 100644 resources/profiles/backend-job.dcdl create mode 100644 resources/prompts/internal/backend_job_system.md diff --git a/crates/manifest/src/builtin_profile.rs b/crates/manifest/src/builtin_profile.rs index 7c0948d3b..b2d291787 100644 --- a/crates/manifest/src/builtin_profile.rs +++ b/crates/manifest/src/builtin_profile.rs @@ -94,6 +94,13 @@ pub const BUILTIN_PROFILE_RESOURCES: &[BuiltinProfileResource] = &[ description: "Workspace orchestration and Worker control.", imports: BASE_IMPORT, }, + BuiltinProfileResource { + selector: Some("builtin:backend-job"), + path: "profiles/backend-job.dcdl", + source: include_str!("../../../resources/profiles/backend-job.dcdl"), + description: "Backend-owned bounded Job Worker.", + imports: BASE_IMPORT, + }, BuiltinProfileResource { selector: Some("builtin:memory-consolidation"), path: "profiles/memory-consolidation.dcdl", diff --git a/crates/protocol/src/subscription.rs b/crates/protocol/src/subscription.rs index d53f5290f..8a036b05b 100644 --- a/crates/protocol/src/subscription.rs +++ b/crates/protocol/src/subscription.rs @@ -631,6 +631,24 @@ impl SubscriptionWorkerWorkdirAttachment { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct SubscriptionWorkerJob { + pub job_id: String, + pub attempt_id: String, + pub purpose: String, +} + +impl SubscriptionWorkerJob { + pub fn validate(&self) -> Result<(), SubscriptionValidationError> { + validate_identifier("job_id", &self.job_id, MAX_RESOURCE_ID_BYTES)?; + validate_identifier("attempt_id", &self.attempt_id, MAX_RESOURCE_ID_BYTES)?; + validate_identifier("job_purpose", &self.purpose, MAX_RESOURCE_ID_BYTES) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] @@ -664,6 +682,10 @@ pub struct SubscriptionWorker { pub display_name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub profile: Option, + /// Backend-owned Job binding. Runtime producers leave this unset; the + /// Workspace projection enriches both snapshots and live updates. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub job: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub workdir_attachments: Vec, } @@ -677,6 +699,9 @@ impl SubscriptionWorker { if let Some(resource_key) = &self.resource_key { validate_identifier("resource_key", resource_key, MAX_RESOURCE_ID_BYTES)?; } + if let Some(job) = &self.job { + job.validate()?; + } let mut aliases = HashSet::new(); let mut workdir_ids = HashSet::new(); for attachment in &self.workdir_attachments { @@ -952,6 +977,7 @@ mod tests { workspace_id: Some("workspace-1".to_string()), display_name: Some(format!("Worker {value}")), profile: Some("builtin:coder".to_string()), + job: None, workdir_attachments: Vec::new(), } } diff --git a/crates/protocol/src/typescript.rs b/crates/protocol/src/typescript.rs index d0a051dae..3d412f163 100644 --- a/crates/protocol/src/typescript.rs +++ b/crates/protocol/src/typescript.rs @@ -28,7 +28,7 @@ use crate::{ SubscriptionRequest, SubscriptionRequestId, SubscriptionResponse, SubscriptionSnapshot, SubscriptionTerminationCode, SubscriptionWorkdirId, SubscriptionWorker, SubscriptionWorkerAvailability, SubscriptionWorkerId, SubscriptionWorkerIds, - SubscriptionWorkerProtocolMethod, SubscriptionWorkerState, + SubscriptionWorkerJob, SubscriptionWorkerProtocolMethod, SubscriptionWorkerState, SubscriptionWorkerWorkdirAttachment, WorkspaceSubscriptionWorkdir, }, }; @@ -132,6 +132,7 @@ pub fn generated_protocol_types() -> String { push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); @@ -232,6 +233,7 @@ fn rust_serialized_subscription_frame_fixtures() -> Vec { workspace_id: Some("fixture-workspace".to_string()), display_name: None, profile: None, + job: None, workdir_attachments: vec![SubscriptionWorkerWorkdirAttachment { alias: "external".to_string(), repository_id: None, diff --git a/crates/worker-runtime/src/runtime.rs b/crates/worker-runtime/src/runtime.rs index 48b43dbc9..949e75ff8 100644 --- a/crates/worker-runtime/src/runtime.rs +++ b/crates/worker-runtime/src/runtime.rs @@ -4180,6 +4180,7 @@ impl RuntimeState { workspace_id: worker.workspace_id.clone(), display_name: worker.display_name.clone(), profile, + job: None, workdir_attachments, }) } diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index 42200fd82..de5d836f5 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -1225,6 +1225,20 @@ where let worker_enabled = feature_config.worker.enabled; let sub_worker_enabled = feature_config.sub_worker.enabled; let mut feature_registry = FeatureRegistryBuilder::new(); + if worker.manifest().engine.instruction == "internal.backend_job_system" { + let workspace_client = worker.workspace_client_handle(); + if !workspace_client.is_available() || workspace_client.workspace_id().is_none() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Backend Job result capability requires Backend Workspace API authority", + )); + } + feature_registry.add_module( + crate::feature::builtin::backend_job_result::BackendJobResultFeature::new( + workspace_client, + ), + ); + } let memory_install_plan = crate::feature::builtin::memory::MemoryFeatureInstallPlan::prepare( worker.manifest(), worker.workspace_client_handle(), diff --git a/crates/worker/src/feature/builtin.rs b/crates/worker/src/feature/builtin.rs index 800a906db..417c9ca80 100644 --- a/crates/worker/src/feature/builtin.rs +++ b/crates/worker/src/feature/builtin.rs @@ -4,6 +4,7 @@ //! same descriptor-approved registry path used by feature modules. They are not //! an external plugin-loading surface. +pub mod backend_job_result; pub mod flow_transition; pub mod manage_workdir; pub mod manage_worker; diff --git a/crates/worker/src/feature/builtin/backend_job_result.rs b/crates/worker/src/feature/builtin/backend_job_result.rs new file mode 100644 index 000000000..93808162a --- /dev/null +++ b/crates/worker/src/feature/builtin/backend_job_result.rs @@ -0,0 +1,175 @@ +//! Structured completion capability for Backend-owned Job Workers. +//! +//! The tool intentionally accepts only the result value. Workspace and Worker +//! identity come from the authenticated Workspace client, while Job, attempt, +//! and input revision are resolved from the Backend-owned binding. + +use std::sync::Arc; + +use agen::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolOutput}; +use async_trait::async_trait; +use schemars::JsonSchema; +use serde::Deserialize; +use serde_json::Value; + +use crate::feature::{ + FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution, + ToolDeclaration, ToolDefinition, +}; +use crate::worker::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod}; + +const TOOL_NAME: &str = "SubmitBackendJobResult"; +const TOOL_DESCRIPTION: &str = "Submit the bounded structured result for this Backend-owned Job attempt. Workspace, Job, attempt, input revision, and source Worker identity are resolved and fenced by Backend authority; normal final prose is not success."; + +#[derive(Debug, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +struct SubmitResultInput { + result: Value, +} + +#[derive(Clone)] +struct SubmitResultTool { + client: Arc, +} + +#[async_trait] +impl Tool for SubmitResultTool { + async fn execute( + &self, + input_json: &str, + _context: ToolExecutionContext, + ) -> Result { + let input: SubmitResultInput = serde_json::from_str(input_json) + .map_err(|error| ToolError::InvalidArgument(error.to_string()))?; + let workspace_id = self.client.workspace_id().ok_or_else(|| { + ToolError::ExecutionFailed( + "Backend Job result submission requires Workspace identity".to_string(), + ) + })?; + let response = self + .client + .execute(WorkspaceRequest::json( + WorkspaceRequestMethod::Post, + format!("/api/w/{workspace_id}/workers/self/backend-job-result"), + serde_json::to_string(&serde_json::json!({ "result": input.result })) + .map_err(|error| ToolError::Internal(error.to_string()))?, + )) + .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?; + if !response.is_success() { + return Err(ToolError::ExecutionFailed(format!( + "Backend Job result submission was rejected with HTTP {}: {}", + response.status, response.body + ))); + } + Ok(ToolOutput { + summary: "Backend Job result accepted".to_string(), + content: Some(response.body), + attachments: Vec::new(), + }) + } +} + +fn definition(client: Arc) -> ToolDefinition { + let schema = serde_json::to_value(schemars::schema_for!(SubmitResultInput)) + .expect("Backend Job result schema must serialize"); + Arc::new(move || { + ( + ToolMeta::new(TOOL_NAME) + .description(TOOL_DESCRIPTION) + .input_schema(schema.clone()), + Arc::new(SubmitResultTool { + client: client.clone(), + }) as Arc, + ) + }) +} + +#[derive(Clone)] +pub struct BackendJobResultFeature { + client: Arc, +} + +impl BackendJobResultFeature { + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +impl FeatureModule for BackendJobResultFeature { + fn descriptor(&self) -> FeatureDescriptor { + FeatureDescriptor::builtin("backend-job-result", "Backend Job result") + .with_description("Single-purpose structured result capability for Backend-owned Jobs.") + .with_tool(ToolDeclaration::new(TOOL_NAME, TOOL_DESCRIPTION)) + } + + fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> { + context.tools().register(ToolContribution::new( + TOOL_NAME, + definition(self.client.clone()), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::feature::FeatureRegistryBuilder; + use crate::hook::HookRegistryBuilder; + use crate::worker::{WorkspaceClientError, WorkspaceResponse}; + use std::sync::Mutex; + + #[derive(Debug, Default)] + struct RecordingClient { + requests: Mutex>, + } + + impl WorkspaceClient for RecordingClient { + fn workspace_id(&self) -> Option<&str> { + Some("workspace-test") + } + fn kind(&self) -> &str { + "test" + } + fn is_available(&self) -> bool { + true + } + fn execute( + &self, + request: WorkspaceRequest, + ) -> Result { + self.requests.lock().unwrap().push(request); + Ok(WorkspaceResponse { + status: 200, + body: "{\"replayed\":false}".to_string(), + }) + } + } + + #[tokio::test] + async fn feature_exposes_only_identity_free_structured_result_input() { + let client = Arc::new(RecordingClient::default()); + let mut pending = Vec::new(); + let mut hooks = HookRegistryBuilder::default(); + let report = FeatureRegistryBuilder::new() + .with_module(BackendJobResultFeature::new(client.clone())) + .install_into_pending(&mut pending, &mut hooks); + assert_eq!(report.installed_tool_names(), [TOOL_NAME]); + let (meta, tool) = pending.remove(0)(); + assert_eq!(meta.input_schema["required"], serde_json::json!(["result"])); + assert!(meta.input_schema["properties"].get("job_id").is_none()); + tool.execute( + r#"{"result":{"valid":true}}"#, + ToolExecutionContext::direct(), + ) + .await + .unwrap(); + let requests = client.requests.lock().unwrap(); + assert_eq!(requests.len(), 1); + assert_eq!(requests[0].method, WorkspaceRequestMethod::Post); + assert!( + requests[0] + .path + .ends_with("/workers/self/backend-job-result") + ); + } +} diff --git a/crates/worker/src/prompt/catalog.rs b/crates/worker/src/prompt/catalog.rs index 9d7bb468a..aeea63ec8 100644 --- a/crates/worker/src/prompt/catalog.rs +++ b/crates/worker/src/prompt/catalog.rs @@ -95,6 +95,7 @@ pub enum WorkerPrompt { MemoryExtractSystem, MemoryConsolidationSystem, FlowVerifierSystem, + BackendJobSystem, NotifyWrapper, InterruptToolResultSummary, InterruptSystemNote, @@ -112,6 +113,7 @@ impl WorkerPrompt { Self::MemoryExtractSystem => "internal.memory_extract_system", Self::MemoryConsolidationSystem => "internal.memory_consolidation_system", Self::FlowVerifierSystem => "internal.flow_verifier_system", + Self::BackendJobSystem => "internal.backend_job_system", Self::NotifyWrapper => "internal.notify_wrapper", Self::InterruptToolResultSummary => "internal.interrupt_tool_result_summary", Self::InterruptSystemNote => "internal.interrupt_system_note", @@ -130,6 +132,7 @@ impl WorkerPrompt { WorkerPrompt::MemoryExtractSystem, WorkerPrompt::MemoryConsolidationSystem, WorkerPrompt::FlowVerifierSystem, + WorkerPrompt::BackendJobSystem, WorkerPrompt::NotifyWrapper, WorkerPrompt::InterruptToolResultSummary, WorkerPrompt::InterruptSystemNote, diff --git a/crates/workspace-server/src/backend_job.rs b/crates/workspace-server/src/backend_job.rs new file mode 100644 index 000000000..0788589e6 --- /dev/null +++ b/crates/workspace-server/src/backend_job.rs @@ -0,0 +1,489 @@ +//! Durable Backend-owned jobs executed by ordinary Runtime-managed Workers. +//! +//! A job is durable intent. An attempt binds that intent to one dedicated +//! Worker, and a result is accepted only through the fenced structured-result +//! API below. Runtime lifecycle and final prose are deliberately not success +//! authority. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use worker_runtime::identity::RuntimeWorkerRef; + +use crate::{Error, Result}; + +pub const BACKEND_JOB_RUNTIME_ID: &str = crate::hosts::EMBEDDED_RUNTIME_ID; +pub const DEFAULT_MAX_CONCURRENT_JOBS: u16 = 8; +pub const ABSOLUTE_MAX_CONCURRENT_JOBS: u16 = 64; +pub const DEFAULT_JOB_TIMEOUT_SECONDS: u32 = 120; +pub const MAX_JOB_TIMEOUT_SECONDS: u32 = 600; +pub const DEFAULT_MAX_RESULT_BYTES: u32 = 16 * 1024; +pub const ABSOLUTE_MAX_RESULT_BYTES: u32 = 64 * 1024; +pub const DEFAULT_MAX_ATTEMPTS: u8 = 2; +pub const ABSOLUTE_MAX_ATTEMPTS: u8 = 3; +pub const MAX_JOB_INPUT_BYTES: usize = 128 * 1024; +pub const MAX_JOB_INSTRUCTION_BYTES: usize = 16 * 1024; +pub const MAX_JOB_PURPOSE_BYTES: usize = 120; +pub const MAX_JOB_REFERENCE_BYTES: usize = 512; +pub const MAX_JOB_FAILURE_BYTES: usize = 1024; +pub const MAX_JOB_DELIVERY_BYTES: usize = 16 * 1024; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BackendJobState { + Pending, + Completed, + Failed, + Unknown, +} + +impl BackendJobState { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Pending => "pending", + Self::Completed => "completed", + Self::Failed => "failed", + Self::Unknown => "unknown", + } + } + + pub(crate) fn parse(value: &str) -> Result { + match value { + "pending" => Ok(Self::Pending), + "completed" => Ok(Self::Completed), + "failed" => Ok(Self::Failed), + "unknown" => Ok(Self::Unknown), + other => Err(Error::Store(format!("unknown Backend Job state `{other}`"))), + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BackendJobAttemptState { + Reserved, + Dispatched, + Completed, + Failed, + Unknown, +} + +impl BackendJobAttemptState { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Reserved => "reserved", + Self::Dispatched => "dispatched", + Self::Completed => "completed", + Self::Failed => "failed", + Self::Unknown => "unknown", + } + } + + pub(crate) fn parse(value: &str) -> Result { + match value { + "reserved" => Ok(Self::Reserved), + "dispatched" => Ok(Self::Dispatched), + "completed" => Ok(Self::Completed), + "failed" => Ok(Self::Failed), + "unknown" => Ok(Self::Unknown), + other => Err(Error::Store(format!( + "unknown Backend Job attempt state `{other}`" + ))), + } + } + + pub fn terminal(self) -> bool { + matches!(self, Self::Completed | Self::Failed | Self::Unknown) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BackendJobDeliveryState { + Pending, + Completed, + Failed, +} + +impl BackendJobDeliveryState { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Pending => "pending", + Self::Completed => "completed", + Self::Failed => "failed", + } + } + + pub(crate) fn parse(value: &str) -> Result { + match value { + "pending" => Ok(Self::Pending), + "completed" => Ok(Self::Completed), + "failed" => Ok(Self::Failed), + other => Err(Error::Store(format!( + "unknown Backend Job delivery state `{other}`" + ))), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct BackendJobLimits { + #[serde(default = "default_max_concurrent_jobs")] + pub max_concurrent_jobs: u16, + #[serde(default = "default_job_timeout_seconds")] + pub timeout_seconds: u32, + #[serde(default = "default_max_result_bytes")] + pub max_result_bytes: u32, + #[serde(default = "default_max_attempts")] + pub max_attempts: u8, +} + +impl Default for BackendJobLimits { + fn default() -> Self { + Self { + max_concurrent_jobs: DEFAULT_MAX_CONCURRENT_JOBS, + timeout_seconds: DEFAULT_JOB_TIMEOUT_SECONDS, + max_result_bytes: DEFAULT_MAX_RESULT_BYTES, + max_attempts: DEFAULT_MAX_ATTEMPTS, + } + } +} + +impl BackendJobLimits { + pub fn validate(&self) -> Result<()> { + if self.max_concurrent_jobs == 0 || self.max_concurrent_jobs > ABSOLUTE_MAX_CONCURRENT_JOBS + { + return Err(Error::InvalidInput(format!( + "Backend Job max_concurrent_jobs must be in 1..={ABSOLUTE_MAX_CONCURRENT_JOBS}" + ))); + } + if self.timeout_seconds == 0 || self.timeout_seconds > MAX_JOB_TIMEOUT_SECONDS { + return Err(Error::InvalidInput(format!( + "Backend Job timeout_seconds must be in 1..={MAX_JOB_TIMEOUT_SECONDS}" + ))); + } + if self.max_result_bytes == 0 || self.max_result_bytes > ABSOLUTE_MAX_RESULT_BYTES { + return Err(Error::InvalidInput(format!( + "Backend Job max_result_bytes must be in 1..={ABSOLUTE_MAX_RESULT_BYTES}" + ))); + } + if self.max_attempts == 0 || self.max_attempts > ABSOLUTE_MAX_ATTEMPTS { + return Err(Error::InvalidInput(format!( + "Backend Job max_attempts must be in 1..={ABSOLUTE_MAX_ATTEMPTS}" + ))); + } + Ok(()) + } +} + +const fn default_max_concurrent_jobs() -> u16 { + DEFAULT_MAX_CONCURRENT_JOBS +} +const fn default_job_timeout_seconds() -> u32 { + DEFAULT_JOB_TIMEOUT_SECONDS +} +const fn default_max_result_bytes() -> u32 { + DEFAULT_MAX_RESULT_BYTES +} +const fn default_max_attempts() -> u8 { + DEFAULT_MAX_ATTEMPTS +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct BackendJobRequest { + /// Stable caller-selected idempotency key within one Workspace. + pub job_id: String, + /// Stable machine-readable use-site name, for example `ticket_item_check`. + pub purpose: String, + /// Revision fenced by the structured result. + pub input_revision: String, + /// Durable domain reference used to reload or explain the input. + pub input_ref: String, + /// Bounded immutable input snapshot. This must not contain credentials. + pub input: Value, + /// Bounded task instruction; the profile supplies the system instruction. + pub instruction: String, + /// Existing configured profile selector. Job dispatch never accepts raw profile source. + pub profile: String, + /// Writer provenance and optional advisory destination, not parent ownership. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source_worker: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub notification_target: Option, + #[serde(default)] + pub limits: BackendJobLimits, +} + +impl BackendJobRequest { + pub fn validate(&self) -> Result<()> { + validate_identifier("job_id", &self.job_id, 256)?; + validate_identifier("purpose", &self.purpose, MAX_JOB_PURPOSE_BYTES)?; + validate_identifier("input_revision", &self.input_revision, 256)?; + validate_bounded_text("input_ref", &self.input_ref, MAX_JOB_REFERENCE_BYTES)?; + validate_bounded_text("instruction", &self.instruction, MAX_JOB_INSTRUCTION_BYTES)?; + validate_identifier("profile", &self.profile, 256)?; + if self.profile != "builtin:backend-job" { + return Err(Error::InvalidInput( + "Backend Jobs must use the dedicated builtin:backend-job profile".to_string(), + )); + } + let input = serde_json::to_vec(&self.input).map_err(|error| { + Error::InvalidInput(format!("serialize Backend Job input: {error}")) + })?; + if input.len() > MAX_JOB_INPUT_BYTES { + return Err(Error::InvalidInput(format!( + "Backend Job input exceeds {MAX_JOB_INPUT_BYTES} bytes" + ))); + } + self.limits.validate() + } + + pub fn fingerprint(&self) -> Result { + self.validate()?; + let encoded = serde_json::to_vec(self).map_err(|error| { + Error::InvalidInput(format!("serialize Backend Job intent: {error}")) + })?; + Ok(sha256(&encoded)) + } + + pub fn worker_input(&self, attempt_id: &str) -> Result { + let input = serde_json::to_string(&self.input).map_err(|error| { + Error::InvalidInput(format!("serialize Backend Job input: {error}")) + })?; + Ok(format!( + "{}\n\nBackend Job envelope (immutable):\njob_id: {}\nattempt_id: {}\ninput_revision: {}\ninput_ref: {}\ninput_json: {}\n\nReturn success only through the structured Backend Job result capability. Final prose and Worker Idle/Stopped state are not result authority.", + self.instruction, self.job_id, attempt_id, self.input_revision, self.input_ref, input + )) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BackendJobRecord { + pub workspace_id: String, + pub request: BackendJobRequest, + pub intent_fingerprint: String, + pub state: BackendJobState, + pub current_attempt: u8, + pub result: Option, + pub result_digest: Option, + pub failure_category: Option, + pub failure_detail: Option, + pub created_at: String, + pub updated_at: String, + pub completed_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BackendJobAttemptRecord { + pub workspace_id: String, + pub job_id: String, + pub attempt_id: String, + pub attempt: u8, + pub input_revision: String, + pub state: BackendJobAttemptState, + pub worker: Option, + pub runtime_run_id: Option, + pub dispatched_at: Option, + pub deadline_at: String, + pub result: Option, + pub result_digest: Option, + pub failure_category: Option, + pub failure_detail: Option, + pub created_at: String, + pub updated_at: String, + pub completed_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BackendJobReservation { + pub job: BackendJobRecord, + pub attempt: BackendJobAttemptRecord, + pub replayed: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct BackendJobResultSubmission { + pub job_id: String, + pub attempt_id: String, + pub input_revision: String, + pub result: Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BackendJobResultAcceptance { + pub job: BackendJobRecord, + pub attempt: BackendJobAttemptRecord, + pub replayed: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct BackendJobWorkerBinding { + pub job_id: String, + pub attempt_id: String, + pub purpose: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct BackendJobDeliveryRecord { + pub workspace_id: String, + pub delivery_id: String, + pub job_id: String, + pub attempt_id: String, + pub target: RuntimeWorkerRef, + pub state: BackendJobDeliveryState, + pub failure_category: Option, + pub failure_detail: Option, + pub created_at: String, + pub updated_at: String, + pub delivered_at: Option, +} + +pub fn attempt_id(job_id: &str, attempt: u8) -> String { + format!("{job_id}:attempt:{attempt}") +} + +pub fn allocation_key(attempt_id: &str) -> String { + format!("backend-job:{attempt_id}") +} + +pub fn result_digest(result: &Value, max_bytes: u32) -> Result<(String, String)> { + let encoded = serde_json::to_string(result) + .map_err(|error| Error::InvalidInput(format!("serialize Backend Job result: {error}")))?; + if encoded.len() > max_bytes as usize { + return Err(Error::InvalidInput(format!( + "Backend Job result exceeds {max_bytes} bytes" + ))); + } + Ok((sha256(encoded.as_bytes()), encoded)) +} + +pub(crate) fn bounded_failure_detail(detail: &str) -> String { + if detail.len() <= MAX_JOB_FAILURE_BYTES { + return detail.to_string(); + } + let mut end = MAX_JOB_FAILURE_BYTES; + while !detail.is_char_boundary(end) { + end -= 1; + } + detail[..end].to_string() +} + +fn validate_identifier(field: &str, value: &str, max_bytes: usize) -> Result<()> { + validate_bounded_text(field, value, max_bytes)?; + if value.chars().any(char::is_control) { + return Err(Error::InvalidInput(format!( + "Backend Job {field} must not contain control characters" + ))); + } + Ok(()) +} + +fn validate_bounded_text(field: &str, value: &str, max_bytes: usize) -> Result<()> { + if value.is_empty() || value.trim() != value { + return Err(Error::InvalidInput(format!( + "Backend Job {field} must be non-empty and trimmed" + ))); + } + if value.len() > max_bytes { + return Err(Error::InvalidInput(format!( + "Backend Job {field} exceeds {max_bytes} bytes" + ))); + } + Ok(()) +} + +fn sha256(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut output = String::with_capacity(7 + digest.len() * 2); + output.push_str("sha256:"); + for byte in digest { + use std::fmt::Write as _; + let _ = write!(output, "{byte:02x}"); + } + output +} + +#[cfg(test)] +mod tests { + use super::*; + + fn request() -> BackendJobRequest { + BackendJobRequest { + job_id: "ticket-check:T-1:r7".to_string(), + purpose: "ticket_item_check".to_string(), + input_revision: "7".to_string(), + input_ref: "ticket://T-1/revisions/7".to_string(), + input: serde_json::json!({"title": "Check me"}), + instruction: "Check this immutable Ticket snapshot.".to_string(), + profile: "builtin:backend-job".to_string(), + source_worker: Some(RuntimeWorkerRef::new("runtime-a", "worker-a")), + notification_target: Some(RuntimeWorkerRef::new("runtime-a", "worker-a")), + limits: BackendJobLimits::default(), + } + } + + #[test] + fn fingerprint_covers_revision_profile_limits_and_notification_provenance() { + let request = request(); + let first = request.fingerprint().unwrap(); + assert_eq!(first, request.fingerprint().unwrap()); + for mutate in [ + |request: &mut BackendJobRequest| request.input_revision = "8".to_string(), + |request: &mut BackendJobRequest| request.purpose = "alternate_check".to_string(), + |request: &mut BackendJobRequest| request.limits.max_attempts = 3, + |request: &mut BackendJobRequest| request.notification_target = None, + ] { + let mut changed = request.clone(); + mutate(&mut changed); + assert_ne!(first, changed.fingerprint().unwrap()); + } + } + + #[test] + fn result_is_bounded_before_digesting() { + let value = serde_json::json!({"ok": true}); + let (digest, encoded) = result_digest(&value, 128).unwrap(); + assert!(digest.starts_with("sha256:")); + assert_eq!(encoded, r#"{"ok":true}"#); + assert!(result_digest(&serde_json::json!({"body": "too long"}), 4).is_err()); + } + + #[test] + fn failure_detail_is_bounded_by_utf8_bytes_without_splitting_code_points() { + let detail = "界".repeat(MAX_JOB_FAILURE_BYTES); + let bounded = bounded_failure_detail(&detail); + assert!(bounded.len() <= MAX_JOB_FAILURE_BYTES); + assert!(detail.starts_with(&bounded)); + } + + #[test] + fn job_limits_reject_unbounded_execution_and_retry() { + for limits in [ + BackendJobLimits { + timeout_seconds: 0, + ..Default::default() + }, + BackendJobLimits { + timeout_seconds: MAX_JOB_TIMEOUT_SECONDS + 1, + ..Default::default() + }, + BackendJobLimits { + max_attempts: 0, + ..Default::default() + }, + BackendJobLimits { + max_attempts: ABSOLUTE_MAX_ATTEMPTS + 1, + ..Default::default() + }, + BackendJobLimits { + max_concurrent_jobs: 0, + ..Default::default() + }, + ] { + assert!(limits.validate().is_err()); + } + } +} diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index 6e86ec96a..91e640121 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -544,6 +544,11 @@ pub enum WorkerSpawnIntent { WorkspaceCompanion, WorkspaceOrchestrator, WorkspaceCoding, + BackendJob { + job_id: String, + attempt_id: String, + purpose: String, + }, TicketRole { ticket_id: String, role: TicketWorkerRole, @@ -5890,6 +5895,7 @@ fn worker_spawn_intent_label(intent: &WorkerSpawnIntent) -> &'static str { WorkerSpawnIntent::WorkspaceCompanion => "workspace_companion", WorkerSpawnIntent::WorkspaceOrchestrator => "workspace_orchestrator", WorkerSpawnIntent::WorkspaceCoding => "workspace_coding", + WorkerSpawnIntent::BackendJob { .. } => "backend_job", WorkerSpawnIntent::TicketRole { role, .. } => match role { TicketWorkerRole::Intake => "ticket_intake", TicketWorkerRole::Orchestrator => "ticket_orchestrator", diff --git a/crates/workspace-server/src/latest_schema.sql b/crates/workspace-server/src/latest_schema.sql index 0accd1d9a..ecd814e24 100644 --- a/crates/workspace-server/src/latest_schema.sql +++ b/crates/workspace-server/src/latest_schema.sql @@ -806,6 +806,79 @@ CREATE TABLE "worker_registry" ( PRIMARY KEY (workspace_id, worker_id), FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ); +CREATE TABLE backend_jobs ( + workspace_id TEXT NOT NULL, + job_id TEXT NOT NULL, + purpose TEXT NOT NULL, + input_revision TEXT NOT NULL, + input_ref TEXT NOT NULL, + request_json TEXT NOT NULL, + intent_fingerprint TEXT NOT NULL, + state TEXT NOT NULL CHECK (state IN ('pending', 'completed', 'failed', 'unknown')), + current_attempt INTEGER NOT NULL CHECK (current_attempt > 0 AND current_attempt <= 3), + result_json TEXT, + result_digest TEXT, + failure_category TEXT, + failure_detail TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + completed_at TEXT, + PRIMARY KEY (workspace_id, job_id), + FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + CHECK ((result_json IS NULL) = (result_digest IS NULL)), + CHECK ((state = 'completed') = (result_json IS NOT NULL)) + ); +CREATE INDEX backend_jobs_active + ON backend_jobs(workspace_id, state, updated_at); +CREATE TABLE backend_job_attempts ( + workspace_id TEXT NOT NULL, + job_id TEXT NOT NULL, + attempt_id TEXT NOT NULL, + attempt INTEGER NOT NULL CHECK (attempt > 0 AND attempt <= 3), + input_revision TEXT NOT NULL, + state TEXT NOT NULL CHECK (state IN ('reserved', 'dispatched', 'completed', 'failed', 'unknown')), + runtime_id TEXT, + worker_id TEXT, + runtime_run_id TEXT, + dispatched_at TEXT, + deadline_at TEXT NOT NULL, + result_json TEXT, + result_digest TEXT, + failure_category TEXT, + failure_detail TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + completed_at TEXT, + PRIMARY KEY (workspace_id, job_id, attempt_id), + UNIQUE (workspace_id, job_id, attempt), + UNIQUE (workspace_id, runtime_id, worker_id), + FOREIGN KEY (workspace_id, job_id) + REFERENCES backend_jobs(workspace_id, job_id) ON DELETE CASCADE, + CHECK ((runtime_id IS NULL) = (worker_id IS NULL)), + CHECK ((result_json IS NULL) = (result_digest IS NULL)), + CHECK ((state = 'completed') = (result_json IS NOT NULL)) + ); +CREATE INDEX backend_job_attempts_recovery + ON backend_job_attempts(workspace_id, state, deadline_at); +CREATE TABLE backend_job_deliveries ( + workspace_id TEXT NOT NULL, + delivery_id TEXT NOT NULL, + job_id TEXT NOT NULL, + attempt_id TEXT NOT NULL, + target_runtime_id TEXT NOT NULL, + target_worker_id TEXT NOT NULL, + state TEXT NOT NULL CHECK (state IN ('pending', 'completed', 'failed')), + failure_category TEXT, + failure_detail TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + delivered_at TEXT, + PRIMARY KEY (workspace_id, delivery_id), + FOREIGN KEY (workspace_id, job_id, attempt_id) + REFERENCES backend_job_attempts(workspace_id, job_id, attempt_id) ON DELETE CASCADE + ); +CREATE INDEX backend_job_deliveries_pending + ON backend_job_deliveries(workspace_id, state, updated_at); CREATE TABLE worker_registry_observations ( workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs index cda247556..7d8a86282 100644 --- a/crates/workspace-server/src/lib.rs +++ b/crates/workspace-server/src/lib.rs @@ -6,6 +6,7 @@ pub mod auth; pub mod authority; +pub mod backend_job; pub mod companion; pub mod config; pub mod config_source; diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 1b50907ce..dbeb59ad0 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -13,9 +13,7 @@ use axum::http::header::{CONTENT_TYPE, LOCATION, ORIGIN}; use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri}; use axum::middleware::{self, Next}; use axum::response::{IntoResponse, Response}; -use axum::routing::get; -#[cfg(test)] -use axum::routing::post; +use axum::routing::{get, post}; use axum::{Json, Router}; use chrono::{Duration, SecondsFormat, Utc}; use config_source::ConfigTreeSnapshot; @@ -145,6 +143,11 @@ use crate::authority::{ MemoryAuthority, ObjectiveAuthority, ObjectiveCreateInput, ObjectiveEditInput, SqliteWorkspaceAuthority, TicketAuthority, TicketMergeRevisionSource, merge_request_summary, }; +use crate::backend_job::{ + ABSOLUTE_MAX_CONCURRENT_JOBS, BackendJobAttemptState, BackendJobDeliveryState, + BackendJobRequest, BackendJobReservation, BackendJobResultAcceptance, + BackendJobResultSubmission, BackendJobState, allocation_key, +}; use crate::companion::{ CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse, CompanionStatusResponse, CompanionTranscriptProjection, @@ -3241,6 +3244,7 @@ impl WorkspaceApi { } recover_workdir_removals(&api)?; recover_runtime_removals(&api).await; + api.recover_backend_jobs()?; Ok(api) } @@ -3248,6 +3252,387 @@ impl WorkspaceApi { self.config.workspace_id.as_str() } + /// Reserve durable Job intent and dispatch its current attempt through the + /// ordinary embedded Runtime Worker path. Replaying the same request + /// converges on the same attempt and Worker allocation. + pub fn dispatch_backend_job( + &self, + request: &BackendJobRequest, + ) -> Result { + let now = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true); + let reservation = + self.store + .reserve_backend_job(&self.config.workspace_id, request, &now)?; + self.dispatch_backend_job_reservation(reservation) + } + + /// Explicit re-evaluation creates a new bounded attempt. Transport replay + /// must call `dispatch_backend_job` with the original immutable request. + pub fn retry_backend_job(&self, job_id: &str) -> Result { + let now = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true); + let reservation = + self.store + .reserve_backend_job_retry(&self.config.workspace_id, job_id, &now)?; + self.dispatch_backend_job_reservation(reservation) + } + + fn dispatch_backend_job_reservation( + &self, + reservation: BackendJobReservation, + ) -> Result { + if reservation.job.state != BackendJobState::Pending + || reservation.attempt.state != BackendJobAttemptState::Reserved + || reservation.attempt.worker.is_some() + { + return Ok(reservation); + } + let request = &reservation.job.request; + let worker_input = request.worker_input(&reservation.attempt.attempt_id)?; + let control_operation = WorkerControlOperation { + operation_id: allocation_key(&reservation.attempt.attempt_id), + input_fingerprint: reservation.job.intent_fingerprint.clone(), + }; + let spawned = self + .spawn_workspace_worker( + EMBEDDED_WORKER_RUNTIME_ID, + WorkerSpawnRequest { + requested_worker_name: Some(format!("job:{}", request.purpose)), + intent: WorkerSpawnIntent::BackendJob { + job_id: request.job_id.clone(), + attempt_id: reservation.attempt.attempt_id.clone(), + purpose: request.purpose.clone(), + }, + acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted { + expected_segments: 0, + }, + profile: ProfileSelector::Builtin(request.profile.clone()), + ticket_assignment: None, + initial_submit: Vec::new(), + workdir_attachment_requests: Vec::new(), + resolved_workdir_attachment_requests: Vec::new(), + resolved_workdir_attachments: Vec::new(), + resolved_config_bundle: None, + resolved_worker_observation_enabled: false, + resolved_worker_observation_grants: Vec::new(), + resolved_workspace_api: None, + resolved_memory_settings: None, + resolved_control_operation: Some(control_operation), + }, + ) + .map_err(|error| error.error)?; + let worker = match (spawned.state, spawned.worker) { + (InternalWorkerOperationState::Accepted, Some(worker)) => worker, + _ => { + let detail = spawned + .diagnostics + .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, + &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(), + code: "backend_job_dispatch_rejected".to_string(), + message: detail.to_string(), + }); + } + }; + let bound_at = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true); + let (commit, newly_bound) = self.store.bind_backend_job_attempt_worker( + &self.config.workspace_id, + &request.job_id, + &reservation.attempt.attempt_id, + &worker.worker, + None, + &bound_at, + )?; + if newly_bound { + self.worker_projection.publish_commit(commit)?; + } else { + let job = self + .store + .get_backend_job(&self.config.workspace_id, &request.job_id)? + .ok_or_else(|| Error::Store("bound Backend Job disappeared".to_string()))?; + let attempt = self + .store + .get_backend_job_attempt( + &self.config.workspace_id, + &request.job_id, + &reservation.attempt.attempt_id, + )? + .ok_or_else(|| Error::Store("bound Backend Job attempt disappeared".to_string()))?; + return Ok(BackendJobReservation { + job, + attempt, + replayed: true, + }); + } + + let submitted = self + .runtime + .send_input( + &worker.worker, + WorkerInputRequest { + kind: WorkerInputKind::User, + content: worker_input, + segments: None, + }, + ) + .map_err(RuntimeRegistryError::into_error)?; + if submitted.state != InternalWorkerOperationState::Accepted { + let detail = submitted + .diagnostics + .first() + .map(|diagnostic| diagnostic.message.as_str()) + .unwrap_or("embedded Runtime rejected Backend Job input"); + self.store.finish_backend_job_attempt( + &self.config.workspace_id, + &request.job_id, + &reservation.attempt.attempt_id, + BackendJobAttemptState::Failed, + "input_rejected", + detail, + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + )?; + return Err(Error::RuntimeOperationFailed { + runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(), + code: "backend_job_input_rejected".to_string(), + message: detail.to_string(), + }); + } + self.schedule_backend_job_timeout( + &request.job_id, + &reservation.attempt.attempt_id, + &reservation.attempt.deadline_at, + ); + let job = self + .store + .get_backend_job(&self.config.workspace_id, &request.job_id)? + .ok_or_else(|| Error::Store("dispatched Backend Job disappeared".to_string()))?; + let attempt = self + .store + .get_backend_job_attempt( + &self.config.workspace_id, + &request.job_id, + &reservation.attempt.attempt_id, + )? + .ok_or_else(|| { + Error::Store("dispatched Backend Job attempt disappeared".to_string()) + })?; + Ok(BackendJobReservation { + job, + attempt, + replayed: reservation.replayed, + }) + } + + /// 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 + /// known, and always receive a fresh deadline task. + pub fn recover_backend_jobs(&self) -> Result<()> { + let attempts = self.store.list_recoverable_backend_job_attempts( + &self.config.workspace_id, + usize::from(ABSOLUTE_MAX_CONCURRENT_JOBS), + )?; + for attempt in attempts { + match attempt.state { + BackendJobAttemptState::Reserved => { + let job = self + .store + .get_backend_job(&self.config.workspace_id, &attempt.job_id)? + .ok_or_else(|| { + Error::Store("recoverable Backend Job disappeared".to_string()) + })?; + if let Err(error) = + self.dispatch_backend_job_reservation(BackendJobReservation { + job, + attempt, + replayed: true, + }) + { + tracing::warn!(%error, "Backend Job reserved-attempt recovery failed"); + } + } + BackendJobAttemptState::Dispatched => { + let worker_known = attempt + .worker + .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, + &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( + &attempt.job_id, + &attempt.attempt_id, + &attempt.deadline_at, + ); + } + } + BackendJobAttemptState::Completed + | BackendJobAttemptState::Failed + | BackendJobAttemptState::Unknown => {} + } + } + Ok(()) + } + + fn schedule_backend_job_timeout(&self, job_id: &str, attempt_id: &str, deadline_at: &str) { + let Ok(deadline) = chrono::DateTime::parse_from_rfc3339(deadline_at) else { + return; + }; + let delay = (deadline.with_timezone(&Utc) - Utc::now()) + .to_std() + .unwrap_or_default(); + let Ok(handle) = tokio::runtime::Handle::try_current() else { + return; + }; + let store = self.store.clone(); + let runtime = self.runtime.clone(); + let workspace_id = self.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) + else { + return; + }; + if attempt.state != BackendJobAttemptState::Dispatched { + return; + } + // Fence result acceptance before stopping the Worker so a late + // completion cannot turn a timed-out attempt into success. + if store + .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, + }, + ); + } + } + }); + } + + /// Accept the only authoritative success signal for a Job attempt. The + /// source identity is fenced against the durable Worker binding. Result + /// persistence precedes best-effort notification delivery. + pub fn accept_backend_job_result( + &self, + source_worker: &RuntimeWorkerRef, + submission: &BackendJobResultSubmission, + ) -> Result { + let accepted_at = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true); + let acceptance = self.store.accept_backend_job_result( + &self.config.workspace_id, + source_worker, + submission, + &accepted_at, + )?; + if let Some(target) = acceptance.job.request.notification_target.as_ref() { + self.deliver_backend_job_result(&acceptance, target); + } + Ok(acceptance) + } + + fn deliver_backend_job_result( + &self, + acceptance: &BackendJobResultAcceptance, + target: &RuntimeWorkerRef, + ) { + let delivery_id = format!( + "{}:{}:notification", + acceptance.job.request.job_id, acceptance.attempt.attempt_id + ); + let now = Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true); + let Ok(delivery) = self.store.reserve_backend_job_delivery( + &self.config.workspace_id, + &delivery_id, + &acceptance.job.request.job_id, + target, + &now, + ) else { + return; + }; + if delivery.state == BackendJobDeliveryState::Completed { + return; + } + let content = format!( + "Backend Job `{}` completed attempt `{}` for `{}`. Durable result digest: {}.", + acceptance.job.request.job_id, + acceptance.attempt.attempt_id, + acceptance.job.request.purpose, + acceptance + .job + .result_digest + .as_deref() + .unwrap_or("unavailable") + ); + let delivery_result = self.runtime.send_input( + target, + WorkerInputRequest { + kind: WorkerInputKind::Notify, + content, + segments: None, + }, + ); + let (state, category, detail): (_, Option, Option) = match delivery_result { + Ok(result) if result.state == InternalWorkerOperationState::Accepted => { + (BackendJobDeliveryState::Completed, None, None) + } + Ok(result) => ( + BackendJobDeliveryState::Failed, + Some("notification_rejected".to_string()), + result.diagnostics.first().map(|item| item.message.clone()), + ), + Err(error) => ( + BackendJobDeliveryState::Failed, + Some("notification_unavailable".to_string()), + Some(error.message()), + ), + }; + // Delivery is intentionally separate from result authority. A delivery + // bookkeeping failure must never roll back or re-run completed work. + let _ = self.store.finish_backend_job_delivery( + &self.config.workspace_id, + &delivery_id, + state, + category.as_deref(), + detail.as_deref(), + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + ); + } + fn register_workspace_runtime_binding( &self, binding: WorkspaceRuntimeBinding, @@ -9123,6 +9508,10 @@ fn build_inner_router(api: WorkspaceApi) -> Router { _, _, >(workspace, scoped_worker_protocol_ws) + .route( + "/api/w/{workspace_id}/workers/self/backend-job-result", + post(scoped_submit_backend_job_result), + ) .fallback(get(static_or_spa_fallback)) .with_state(api) .merge(generated_workspace_contract_router(contract_service)); @@ -14474,6 +14863,49 @@ async fn refresh_current_worker_session_locked( Ok(()) } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SubmitBackendJobResultRequest { + result: serde_json::Value, +} + +async fn scoped_submit_backend_job_result( + State(api): State, + AxumPath(path): AxumPath, + headers: HeaderMap, + Json(request): Json, +) -> ApiResult> { + validate_workspace_scope(&api, &path.workspace_id)?; + let worker = current_worker_identity(&api, &path.workspace_id, &headers)?; + let binding = api + .store + .worker_registry_projection(&api.config.workspace_id, &worker)? + .and_then(|projection| projection.job) + .ok_or_else(|| { + Error::InvalidInput( + "current Worker is not bound to an active Backend Job attempt".to_string(), + ) + })?; + let attempt = api + .store + .get_backend_job_attempt( + &api.config.workspace_id, + &binding.job_id, + &binding.attempt_id, + )? + .ok_or_else(|| Error::Store("Backend Job attempt binding disappeared".to_string()))?; + let acceptance = api.accept_backend_job_result( + &worker, + &BackendJobResultSubmission { + job_id: binding.job_id, + attempt_id: binding.attempt_id, + input_revision: attempt.input_revision, + result: request.result, + }, + )?; + Ok(Json(acceptance)) +} + async fn scoped_attach_current_worker_workdir( State(api): State, AxumPath(path): AxumPath, @@ -24800,6 +25232,7 @@ fn record_worker_summary( workspace_id: worker.workspace.workspace_id.clone(), display_name: Some(worker.display_name.clone()), profile: worker.profile.clone(), + job: None, workdir_attachments: Vec::new(), }; api.worker_projection @@ -26714,6 +27147,7 @@ mod tests { workspace_id: Some(TEST_WORKSPACE_ID.to_string()), display_name: Some("Worker A".to_string()), profile: Some("builtin:coder".to_string()), + job: None, workdir_attachments: Vec::new(), }, availability, @@ -26723,6 +27157,7 @@ mod tests { projection_revision: 9, observed_at: "2026-09-24T00:00:01Z".to_string(), }), + job: None, }; let unavailable = worker_summary_from_projection(&projection).unwrap(); @@ -30275,6 +30710,7 @@ mod tests { workspace_id: Some(TEST_WORKSPACE_ID.to_string()), display_name: Some("Paused Worker".to_string()), profile: Some("builtin:coder".to_string()), + job: None, workdir_attachments: Vec::new(), }, "4", @@ -33403,6 +33839,297 @@ mod tests { config } + #[tokio::test] + async fn backend_job_runner_uses_registered_embedded_worker_and_structured_result_authority() { + let temp = tempfile::tempdir().unwrap(); + let (api, execution) = test_api_with_recording_backend(temp.path()).await; + let request = BackendJobRequest { + job_id: "runner-check-1".to_string(), + purpose: "runner_contract_check".to_string(), + input_revision: "revision-1".to_string(), + input_ref: "test://runner/input/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 stale_reservation = api + .store + .reserve_backend_job( + TEST_WORKSPACE_ID, + &request, + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + ) + .unwrap(); + let dispatched = api + .dispatch_backend_job_reservation(stale_reservation.clone()) + .unwrap(); + let concurrent_replay = api + .dispatch_backend_job_reservation(stale_reservation) + .unwrap(); + assert!(concurrent_replay.replayed); + assert_eq!( + execution.take_inputs().len(), + 1, + "a stale concurrent dispatch replay must not submit the attempt twice" + ); + assert_eq!(dispatched.attempt.state, BackendJobAttemptState::Dispatched); + let worker = dispatched.attempt.worker.clone().expect("bound Job Worker"); + assert_eq!(worker.runtime_id, EMBEDDED_WORKER_RUNTIME_ID); + assert!( + api.store + .get_worker_registry(TEST_WORKSPACE_ID, &worker) + .unwrap() + .is_some(), + "Job dispatch must use the ordinary Workspace Worker registry" + ); + assert_eq!( + api.store + .worker_registry_projection(TEST_WORKSPACE_ID, &worker) + .unwrap() + .unwrap() + .job, + Some(crate::backend_job::BackendJobWorkerBinding { + job_id: request.job_id.clone(), + attempt_id: dispatched.attempt.attempt_id.clone(), + purpose: request.purpose.clone(), + }) + ); + let replay = api.dispatch_backend_job(&request).unwrap(); + assert!(replay.replayed); + assert_eq!(replay.attempt.worker, Some(worker.clone())); + + let accepted = api + .accept_backend_job_result( + &worker, + &BackendJobResultSubmission { + job_id: request.job_id.clone(), + attempt_id: dispatched.attempt.attempt_id, + input_revision: request.input_revision, + result: serde_json::json!({"valid": true}), + }, + ) + .unwrap(); + assert_eq!(accepted.job.state, BackendJobState::Completed); + } + + #[tokio::test] + async fn backend_job_input_failure_is_durable_and_retryable_without_reusing_attempt() { + let temp = tempfile::tempdir().unwrap(); + let (api, execution) = test_api_with_recording_backend(temp.path()).await; + execution.reject_inputs("model transport unavailable"); + let request = BackendJobRequest { + job_id: "runner-input-failure-1".to_string(), + purpose: "failure_contract_check".to_string(), + input_revision: "revision-1".to_string(), + input_ref: "test://runner/failure/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(), + }; + + assert!(api.dispatch_backend_job(&request).is_err()); + let failed = api + .store + .get_backend_job_attempt( + TEST_WORKSPACE_ID, + &request.job_id, + &crate::backend_job::attempt_id(&request.job_id, 1), + ) + .unwrap() + .unwrap(); + assert_eq!(failed.state, BackendJobAttemptState::Failed); + assert_eq!(failed.failure_category.as_deref(), Some("input_rejected")); + let _retry_error = api.retry_backend_job(&request.job_id).unwrap_err(); + let second = api + .store + .get_backend_job_attempt( + TEST_WORKSPACE_ID, + &request.job_id, + &crate::backend_job::attempt_id(&request.job_id, 2), + ) + .unwrap() + .unwrap(); + assert_eq!(second.attempt, 2); + assert_eq!(second.state, BackendJobAttemptState::Failed); + } + + #[tokio::test] + async fn backend_job_recovery_dispatches_reserved_attempt_without_duplicate_worker() { + let temp = tempfile::tempdir().unwrap(); + let api = test_api(temp.path()).await; + let request = BackendJobRequest { + job_id: "runner-restart-1".to_string(), + purpose: "restart_contract_check".to_string(), + input_revision: "revision-1".to_string(), + input_ref: "test://runner/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 reserved = api + .store + .reserve_backend_job( + TEST_WORKSPACE_ID, + &request, + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + ) + .unwrap(); + assert_eq!(reserved.attempt.state, BackendJobAttemptState::Reserved); + api.recover_backend_jobs().unwrap(); + + let recovered = api + .store + .get_backend_job_attempt( + TEST_WORKSPACE_ID, + &request.job_id, + &reserved.attempt.attempt_id, + ) + .unwrap() + .unwrap(); + assert_eq!(recovered.state, BackendJobAttemptState::Dispatched); + let recovered_worker = recovered.worker.clone().expect("recovered Job Worker"); + let replay = api.dispatch_backend_job(&request).unwrap(); + assert!(replay.replayed); + assert_eq!(replay.attempt.worker, Some(recovered_worker)); + } + + #[tokio::test] + async fn backend_job_recovery_marks_unreconcilable_worker_execution_unknown() { + let temp = tempfile::tempdir().unwrap(); + let api = test_api(temp.path()).await; + let request = BackendJobRequest { + job_id: "runner-unknown-1".to_string(), + purpose: "unknown_contract_check".to_string(), + input_revision: "revision-1".to_string(), + input_ref: "test://runner/unknown/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 reserved = api + .store + .reserve_backend_job( + TEST_WORKSPACE_ID, + &request, + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + ) + .unwrap(); + let missing_worker = RuntimeWorkerRef::new( + EMBEDDED_WORKER_RUNTIME_ID, + "0199a3ae-ecdf-7eb2-883e-a5b7b5f04a0d", + ); + api.store + .upsert_worker_registry(&WorkerRegistryRecord { + workspace_id: TEST_WORKSPACE_ID.to_string(), + worker: missing_worker.clone(), + display_name: "Missing Job Worker".to_string(), + profile: Some("builtin:backend-job".to_string()), + retention_state: "normal".to_string(), + transcript_ref: None, + session_ref: None, + summary_ref: None, + diagnostics_ref: None, + created_at: "2026-09-01T00:00:00Z".to_string(), + updated_at: "2026-09-01T00:00:00Z".to_string(), + }) + .unwrap(); + api.store + .bind_backend_job_attempt_worker( + TEST_WORKSPACE_ID, + &request.job_id, + &reserved.attempt.attempt_id, + &missing_worker, + Some("missing-runtime-run"), + &Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true), + ) + .unwrap(); + + api.recover_backend_jobs().unwrap(); + let attempt = api + .store + .get_backend_job_attempt( + TEST_WORKSPACE_ID, + &request.job_id, + &reserved.attempt.attempt_id, + ) + .unwrap() + .unwrap(); + assert_eq!(attempt.state, BackendJobAttemptState::Unknown); + assert_eq!( + attempt.failure_category.as_deref(), + Some("recovery_worker_unknown") + ); + assert_eq!( + api.store + .get_backend_job(TEST_WORKSPACE_ID, &request.job_id) + .unwrap() + .unwrap() + .state, + BackendJobState::Unknown + ); + } + + #[tokio::test] + async fn backend_job_timeout_fences_late_structured_result() { + let temp = tempfile::tempdir().unwrap(); + let api = test_api(temp.path()).await; + let request = BackendJobRequest { + job_id: "runner-timeout-1".to_string(), + purpose: "timeout_contract_check".to_string(), + input_revision: "revision-1".to_string(), + input_ref: "test://runner/timeout/1".to_string(), + input: serde_json::json!({"value": "immutable"}), + instruction: "Wait for a structured result.".to_string(), + profile: "builtin:backend-job".to_string(), + source_worker: None, + notification_target: None, + limits: crate::backend_job::BackendJobLimits { + timeout_seconds: 1, + max_attempts: 1, + ..Default::default() + }, + }; + let dispatched = api.dispatch_backend_job(&request).unwrap(); + let worker = dispatched.attempt.worker.clone().unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(1_200)).await; + let attempt = api + .store + .get_backend_job_attempt( + TEST_WORKSPACE_ID, + &request.job_id, + &dispatched.attempt.attempt_id, + ) + .unwrap() + .unwrap(); + assert_eq!(attempt.state, BackendJobAttemptState::Failed); + assert_eq!(attempt.failure_category.as_deref(), Some("timeout")); + assert!( + api.accept_backend_job_result( + &worker, + &BackendJobResultSubmission { + job_id: request.job_id, + attempt_id: dispatched.attempt.attempt_id, + input_revision: request.input_revision, + result: serde_json::json!({"late": true}), + }, + ) + .is_err() + ); + } + #[tokio::test] async fn scoped_runtime_workdir_access_requires_workspace_binding() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/workspace-server/src/store.rs b/crates/workspace-server/src/store.rs index 1d8769f11..f233b0c2c 100644 --- a/crates/workspace-server/src/store.rs +++ b/crates/workspace-server/src/store.rs @@ -17,11 +17,17 @@ use uuid::Uuid; use server_api::{RepositoryObservedStatus, RepositorySource}; use worker_runtime::identity::{RuntimeWorkerRef, WorkerId}; +use crate::backend_job::{ + BackendJobAttemptRecord, BackendJobAttemptState, BackendJobDeliveryRecord, + BackendJobDeliveryState, BackendJobRecord, BackendJobRequest, BackendJobReservation, + BackendJobResultAcceptance, BackendJobResultSubmission, BackendJobState, + BackendJobWorkerBinding, 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 = 69; +const LATEST_SCHEMA_VERSION: i64 = 70; 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"; @@ -54,6 +60,8 @@ const EXTERNAL_WORKDIR_COMMAND_PERMISSION_MIGRATION_NAME: &str = "External Workdir explicit command permission"; const WORKDIR_COMMAND_ATTACHMENT_CAPABILITIES_MIGRATION_NAME: &str = "External Workdir command attachment capabilities"; +const BACKEND_JOB_RUNNER_MIGRATION_NAME: &str = + "Backend-owned Runtime Worker jobs and structured results"; const TICKET_SCHEMA_VERSION_WITH_TARGETS: i64 = 7; const TICKET_SCHEMA_BASELINE_NAME: &str = "ticket schema baseline"; const WORKER_REGISTRY_PROJECTION_SCHEMA: &str = r#" @@ -196,6 +204,11 @@ const MIGRATIONS: &[Migration] = &[ name: WORKDIR_COMMAND_ATTACHMENT_CAPABILITIES_MIGRATION_NAME, apply: migrate_workdir_command_attachment_capabilities_v68_to_v69, }, + Migration { + version: 70, + name: BACKEND_JOB_RUNNER_MIGRATION_NAME, + apply: migrate_backend_job_runner_v69_to_v70, + }, ]; #[derive(Clone, Copy)] @@ -611,6 +624,7 @@ pub struct WorkerRegistryProjectionRecord { pub registry: WorkerRegistryRecord, pub resource_key: Option, pub observation: Option, + pub job: Option, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1443,6 +1457,75 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore { consumed_at: &str, ) -> Result>; + fn reserve_backend_job( + &self, + workspace_id: &str, + request: &BackendJobRequest, + now: &str, + ) -> Result; + fn reserve_backend_job_retry( + &self, + workspace_id: &str, + job_id: &str, + now: &str, + ) -> Result; + fn get_backend_job(&self, workspace_id: &str, job_id: &str) + -> Result>; + fn get_backend_job_attempt( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + ) -> Result>; + fn list_recoverable_backend_job_attempts( + &self, + workspace_id: &str, + limit: usize, + ) -> Result>; + fn bind_backend_job_attempt_worker( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + worker: &RuntimeWorkerRef, + runtime_run_id: Option<&str>, + now: &str, + ) -> Result<(WorkerRegistryProjectionCommit, bool)>; + fn accept_backend_job_result( + &self, + workspace_id: &str, + source_worker: &RuntimeWorkerRef, + submission: &BackendJobResultSubmission, + now: &str, + ) -> Result; + fn finish_backend_job_attempt( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + state: BackendJobAttemptState, + failure_category: &str, + failure_detail: &str, + now: &str, + ) -> Result; + fn reserve_backend_job_delivery( + &self, + workspace_id: &str, + delivery_id: &str, + job_id: &str, + target: &RuntimeWorkerRef, + now: &str, + ) -> Result; + fn finish_backend_job_delivery( + &self, + workspace_id: &str, + delivery_id: &str, + state: BackendJobDeliveryState, + failure_category: Option<&str>, + failure_detail: Option<&str>, + now: &str, + ) -> Result; + fn upsert_worker_registry( &self, record: &WorkerRegistryRecord, @@ -5743,6 +5826,407 @@ impl ControlPlaneStore for SqliteWorkspaceStore { }) } + fn reserve_backend_job( + &self, + workspace_id: &str, + request: &BackendJobRequest, + now: &str, + ) -> Result { + request.validate()?; + let fingerprint = request.fingerprint()?; + let request_json = serde_json::to_string(request).map_err(|error| { + Error::InvalidInput(format!("serialize Backend Job request: {error}")) + })?; + let deadline = backend_job_deadline(now, request.limits.timeout_seconds)?; + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + if let Some(job) = read_backend_job(&tx, workspace_id, &request.job_id)? { + if job.intent_fingerprint != fingerprint { + return Err(Error::InvalidInput(format!( + "Backend Job id `{}` was reused with different intent", + request.job_id + ))); + } + let attempt_id = attempt_id(&request.job_id, job.current_attempt); + let attempt = read_backend_job_attempt(&tx, workspace_id, &request.job_id, &attempt_id)? + .ok_or_else(|| Error::Store("Backend Job current attempt is missing".to_string()))?; + tx.commit()?; + return Ok(BackendJobReservation { job, attempt, replayed: true }); + } + for (role, worker) in [ + ("source", request.source_worker.as_ref()), + ("notification target", request.notification_target.as_ref()), + ] { + if let Some(worker) = worker + && !worker_registry_identity_exists(&tx, workspace_id, worker)? + { + return Err(Error::InvalidInput(format!( + "Backend Job {role} Worker does not belong to this Workspace" + ))); + } + } + ensure_backend_job_capacity(&tx, workspace_id, request.limits.max_concurrent_jobs)?; + let current_attempt = 1_u8; + let attempt_id = attempt_id(&request.job_id, current_attempt); + tx.execute( + "INSERT INTO backend_jobs (workspace_id, job_id, purpose, input_revision, input_ref, request_json, intent_fingerprint, state, current_attempt, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'pending', ?8, ?9, ?9)", + params![workspace_id, request.job_id, request.purpose, request.input_revision, request.input_ref, request_json, fingerprint, current_attempt as i64, now], + )?; + tx.execute( + "INSERT INTO backend_job_attempts (workspace_id, job_id, attempt_id, attempt, input_revision, state, deadline_at, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, 'reserved', ?6, ?7, ?7)", + params![workspace_id, request.job_id, attempt_id, current_attempt as i64, request.input_revision, deadline, now], + )?; + let job = read_backend_job(&tx, workspace_id, &request.job_id)? + .ok_or_else(|| Error::Store("reserved Backend Job is missing".to_string()))?; + let attempt = read_backend_job_attempt(&tx, workspace_id, &request.job_id, &attempt_id)? + .ok_or_else(|| Error::Store("reserved Backend Job attempt is missing".to_string()))?; + tx.commit()?; + Ok(BackendJobReservation { job, attempt, replayed: false }) + }) + } + + fn reserve_backend_job_retry( + &self, + workspace_id: &str, + job_id: &str, + now: &str, + ) -> Result { + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let mut job = read_backend_job(&tx, workspace_id, job_id)? + .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job `{job_id}`")))?; + let previous_id = attempt_id(job_id, job.current_attempt); + let previous = read_backend_job_attempt(&tx, workspace_id, job_id, &previous_id)? + .ok_or_else(|| Error::Store("Backend Job current attempt is missing".to_string()))?; + if !matches!(previous.state, BackendJobAttemptState::Failed | BackendJobAttemptState::Unknown) { + return Err(Error::InvalidInput( + "Backend Job re-evaluation requires a failed or unknown current attempt".to_string(), + )); + } + if job.current_attempt >= job.request.limits.max_attempts { + return Err(Error::InvalidInput(format!( + "Backend Job `{job_id}` reached its attempt limit" + ))); + } + ensure_backend_job_capacity(&tx, workspace_id, job.request.limits.max_concurrent_jobs)?; + let next = job.current_attempt + 1; + let next_id = attempt_id(job_id, next); + let deadline = backend_job_deadline(now, job.request.limits.timeout_seconds)?; + let changed = tx.execute( + "UPDATE backend_jobs SET state = 'pending', current_attempt = ?3, result_json = NULL, result_digest = NULL, failure_category = NULL, failure_detail = NULL, completed_at = NULL, updated_at = ?4 WHERE workspace_id = ?1 AND job_id = ?2 AND current_attempt = ?5", + params![workspace_id, job_id, next as i64, now, job.current_attempt as i64], + )?; + if changed != 1 { + return Err(Error::Store("Backend Job retry compare-and-set failed".to_string())); + } + tx.execute( + "INSERT INTO backend_job_attempts (workspace_id, job_id, attempt_id, attempt, input_revision, state, deadline_at, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, 'reserved', ?6, ?7, ?7)", + params![workspace_id, job_id, next_id, next as i64, job.request.input_revision, deadline, now], + )?; + job = read_backend_job(&tx, workspace_id, job_id)? + .ok_or_else(|| Error::Store("retried Backend Job is missing".to_string()))?; + let attempt = read_backend_job_attempt(&tx, workspace_id, job_id, &next_id)? + .ok_or_else(|| Error::Store("retried Backend Job attempt is missing".to_string()))?; + tx.commit()?; + Ok(BackendJobReservation { job, attempt, replayed: false }) + }) + } + + fn get_backend_job( + &self, + workspace_id: &str, + job_id: &str, + ) -> Result> { + self.with_conn(|conn| read_backend_job(conn, workspace_id, job_id)) + } + + fn get_backend_job_attempt( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + ) -> Result> { + self.with_conn(|conn| read_backend_job_attempt(conn, workspace_id, job_id, attempt_id)) + } + + fn list_recoverable_backend_job_attempts( + &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 ('reserved', 'dispatched') ORDER BY 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 attempt disappeared".to_string())) + }).collect() + }) + } + + fn bind_backend_job_attempt_worker( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + worker: &RuntimeWorkerRef, + runtime_run_id: Option<&str>, + now: &str, + ) -> Result<(WorkerRegistryProjectionCommit, bool)> { + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let job = read_backend_job(&tx, workspace_id, job_id)? + .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job `{job_id}`")))?; + 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.attempt != job.current_attempt || attempt.input_revision != job.request.input_revision { + return Err(Error::InvalidInput( + "Backend Job attempt is stale for the current input revision".to_string(), + )); + } + if let Some(bound) = &attempt.worker { + if bound != worker || attempt.runtime_run_id.as_deref() != runtime_run_id { + return Err(Error::InvalidInput( + "Backend Job attempt is already bound to different Runtime evidence".to_string(), + )); + } + let commit = WorkerRegistryProjectionCommit { + revision: current_worker_projection_revision(&tx, workspace_id)?, + changes: Vec::new(), + }; + tx.commit()?; + return Ok((commit, false)); + } + if !worker_registry_identity_exists(&tx, workspace_id, worker)? { + return Err(Error::InvalidInput( + "Backend Job Worker must be registered before binding".to_string(), + )); + } + let changed = tx.execute( + "UPDATE backend_job_attempts SET state = 'dispatched', runtime_id = ?4, worker_id = ?5, runtime_run_id = ?6, dispatched_at = ?7, updated_at = ?7 WHERE workspace_id = ?1 AND job_id = ?2 AND attempt_id = ?3 AND state = 'reserved' AND worker_id IS NULL", + params![workspace_id, job_id, attempt_id, worker.runtime_id, worker.worker_id, runtime_run_id, now], + )?; + if changed != 1 { + return Err(Error::InvalidInput( + "Backend Job attempt is not available for Worker binding".to_string(), + )); + } + let changed_workers = vec![worker.clone()]; + let revision = commit_worker_projection_changes(&tx, workspace_id, &changed_workers)?; + let changes = worker_catalog_upsert_changes(&tx, workspace_id, &changed_workers)?; + tx.commit()?; + Ok((WorkerRegistryProjectionCommit { revision, changes }, true)) + }) + } + + fn accept_backend_job_result( + &self, + workspace_id: &str, + source_worker: &RuntimeWorkerRef, + submission: &BackendJobResultSubmission, + now: &str, + ) -> Result { + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let job = read_backend_job(&tx, workspace_id, &submission.job_id)? + .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job `{}`", submission.job_id)))?; + let attempt = read_backend_job_attempt(&tx, workspace_id, &submission.job_id, &submission.attempt_id)? + .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job attempt `{}`", submission.attempt_id)))?; + let (digest, result_json) = result_digest(&submission.result, job.request.limits.max_result_bytes)?; + if submission.input_revision != job.request.input_revision + || attempt.input_revision != submission.input_revision + || attempt.attempt != job.current_attempt + { + return Err(Error::InvalidInput( + "Backend Job result is stale for the current attempt or input revision".to_string(), + )); + } + if attempt.worker.as_ref() != Some(source_worker) { + return Err(Error::InvalidInput( + "Backend Job result source is not the attempt Worker".to_string(), + )); + } + if attempt.state == BackendJobAttemptState::Completed { + if attempt.result_digest.as_deref() != Some(digest.as_str()) { + return Err(Error::InvalidInput( + "Backend Job result replay conflicts with the accepted result".to_string(), + )); + } + tx.commit()?; + return Ok(BackendJobResultAcceptance { job, attempt, replayed: true }); + } + if attempt.state != BackendJobAttemptState::Dispatched || job.state != BackendJobState::Pending { + return Err(Error::InvalidInput( + "Backend Job attempt no longer accepts results".to_string(), + )); + } + 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'", + params![workspace_id, submission.job_id, submission.attempt_id, result_json, digest, now], + )?; + let job_changed = tx.execute( + "UPDATE backend_jobs SET state = 'completed', result_json = ?3, result_digest = ?4, failure_category = NULL, failure_detail = NULL, completed_at = ?5, updated_at = ?5 WHERE workspace_id = ?1 AND job_id = ?2 AND state = 'pending'", + params![workspace_id, submission.job_id, result_json, digest, now], + )?; + if attempt_changed != 1 || job_changed != 1 { + return Err(Error::Store("Backend Job result acceptance compare-and-set failed".to_string())); + } + let job = read_backend_job(&tx, workspace_id, &submission.job_id)? + .ok_or_else(|| Error::Store("accepted Backend Job disappeared".to_string()))?; + let attempt = read_backend_job_attempt(&tx, workspace_id, &submission.job_id, &submission.attempt_id)? + .ok_or_else(|| Error::Store("accepted Backend Job attempt disappeared".to_string()))?; + tx.commit()?; + Ok(BackendJobResultAcceptance { job, attempt, replayed: false }) + }) + } + + fn finish_backend_job_attempt( + &self, + workspace_id: &str, + job_id: &str, + attempt_id: &str, + state: BackendJobAttemptState, + failure_category: &str, + failure_detail: &str, + now: &str, + ) -> Result { + if !matches!( + state, + BackendJobAttemptState::Failed | BackendJobAttemptState::Unknown + ) { + return Err(Error::InvalidInput( + "Backend Job failure completion requires failed or unknown state".to_string(), + )); + } + let detail = bounded_failure_detail(failure_detail); + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let job = read_backend_job(&tx, workspace_id, job_id)? + .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job `{job_id}`")))?; + 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 == BackendJobAttemptState::Completed { + return Err(Error::InvalidInput( + "accepted Backend Job result cannot be replaced by failure".to_string(), + )); + } + if attempt.state.terminal() { + if attempt.state != state || attempt.failure_category.as_deref() != Some(failure_category) { + return Err(Error::InvalidInput( + "Backend Job terminal attempt replay conflicts with stored failure".to_string(), + )); + } + tx.commit()?; + 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', '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 { + let job_state = match state { + BackendJobAttemptState::Failed => BackendJobState::Failed, + BackendJobAttemptState::Unknown => BackendJobState::Unknown, + _ => unreachable!(), + }; + tx.execute( + "UPDATE backend_jobs SET state = ?3, failure_category = ?4, failure_detail = ?5, completed_at = ?6, updated_at = ?6 WHERE workspace_id = ?1 AND job_id = ?2 AND state = 'pending'", + params![workspace_id, job_id, job_state.as_str(), failure_category, detail, now], + )?; + } + let attempt = read_backend_job_attempt(&tx, workspace_id, job_id, attempt_id)? + .ok_or_else(|| Error::Store("finished Backend Job attempt disappeared".to_string()))?; + tx.commit()?; + Ok(attempt) + }) + } + + fn reserve_backend_job_delivery( + &self, + workspace_id: &str, + delivery_id: &str, + job_id: &str, + target: &RuntimeWorkerRef, + now: &str, + ) -> Result { + self.with_conn_mut(|conn| { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + if let Some(existing) = read_backend_job_delivery(&tx, workspace_id, delivery_id)? { + if existing.job_id != job_id || existing.target != *target { + return Err(Error::InvalidInput( + "Backend Job delivery id was reused with different intent".to_string(), + )); + } + tx.commit()?; + return Ok(existing); + } + let job = read_backend_job(&tx, workspace_id, job_id)? + .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job `{job_id}`")))?; + if job.state != BackendJobState::Completed { + return Err(Error::InvalidInput( + "Backend Job result must be committed before delivery".to_string(), + )); + } + if job.request.notification_target.as_ref() != Some(target) { + return Err(Error::InvalidInput( + "Backend Job delivery target does not match durable notification intent".to_string(), + )); + } + let current_attempt_id = attempt_id(job_id, job.current_attempt); + tx.execute( + "INSERT INTO backend_job_deliveries (workspace_id, delivery_id, job_id, attempt_id, target_runtime_id, target_worker_id, state, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'pending', ?7, ?7)", + params![workspace_id, delivery_id, job_id, current_attempt_id, target.runtime_id, target.worker_id, now], + )?; + let delivery = read_backend_job_delivery(&tx, workspace_id, delivery_id)? + .ok_or_else(|| Error::Store("reserved Backend Job delivery disappeared".to_string()))?; + tx.commit()?; + Ok(delivery) + }) + } + + fn finish_backend_job_delivery( + &self, + workspace_id: &str, + delivery_id: &str, + state: BackendJobDeliveryState, + failure_category: Option<&str>, + failure_detail: Option<&str>, + now: &str, + ) -> Result { + if state == BackendJobDeliveryState::Pending { + return Err(Error::InvalidInput( + "Backend Job delivery completion cannot remain pending".to_string(), + )); + } + let 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_delivery(&tx, workspace_id, delivery_id)? + .ok_or_else(|| Error::InvalidInput(format!("unknown Backend Job delivery `{delivery_id}`")))?; + if current.state == BackendJobDeliveryState::Completed { + if state != BackendJobDeliveryState::Completed { + return Err(Error::InvalidInput( + "completed Backend Job delivery cannot be replaced".to_string(), + )); + } + tx.commit()?; + return Ok(current); + } + tx.execute( + "UPDATE backend_job_deliveries SET state = ?3, failure_category = ?4, failure_detail = ?5, delivered_at = CASE WHEN ?3 = 'completed' THEN ?6 ELSE delivered_at END, updated_at = ?6 WHERE workspace_id = ?1 AND delivery_id = ?2", + params![workspace_id, delivery_id, state.as_str(), failure_category, detail, now], + )?; + let delivery = read_backend_job_delivery(&tx, workspace_id, delivery_id)? + .ok_or_else(|| Error::Store("finished Backend Job delivery disappeared".to_string()))?; + tx.commit()?; + Ok(delivery) + }) + } + fn upsert_worker_registry( &self, record: &WorkerRegistryRecord, @@ -9281,6 +9765,206 @@ fn read_memory_staging_resolution_record( }) } +fn backend_job_deadline(now: &str, timeout_seconds: u32) -> Result { + let now = chrono::DateTime::parse_from_rfc3339(now) + .map_err(|error| Error::InvalidInput(format!("invalid Backend Job timestamp: {error}")))?; + Ok((now + chrono::Duration::seconds(i64::from(timeout_seconds))).to_rfc3339()) +} + +fn ensure_backend_job_capacity( + conn: &Connection, + workspace_id: &str, + max_concurrent: u16, +) -> Result<()> { + let active: i64 = conn.query_row( + "SELECT COUNT(*) FROM backend_job_attempts WHERE workspace_id = ?1 AND state IN ('reserved', 'dispatched')", + params![workspace_id], + |row| row.get(0), + )?; + if active >= i64::from(max_concurrent) { + return Err(Error::InvalidInput(format!( + "Backend Job concurrency limit {max_concurrent} is reached" + ))); + } + Ok(()) +} + +fn read_backend_job( + conn: &Connection, + workspace_id: &str, + job_id: &str, +) -> Result> { + let row = conn.query_row( + "SELECT request_json, intent_fingerprint, state, current_attempt, result_json, result_digest, failure_category, failure_detail, created_at, updated_at, completed_at FROM backend_jobs WHERE workspace_id = ?1 AND job_id = ?2", + params![workspace_id, job_id], + |row| Ok(( + row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, + row.get::<_, i64>(3)?, row.get::<_, Option>(4)?, row.get::<_, Option>(5)?, + row.get::<_, Option>(6)?, row.get::<_, Option>(7)?, row.get::<_, String>(8)?, + row.get::<_, String>(9)?, row.get::<_, Option>(10)?, + )), + ).optional()?; + let Some(( + request_json, + intent_fingerprint, + state, + current_attempt, + result_json, + result_digest, + failure_category, + failure_detail, + created_at, + updated_at, + completed_at, + )) = row + else { + return Ok(None); + }; + let request: BackendJobRequest = serde_json::from_str(&request_json) + .map_err(|error| Error::Store(format!("invalid stored Backend Job request: {error}")))?; + let result = result_json + .map(|value| serde_json::from_str(&value)) + .transpose() + .map_err(|error| Error::Store(format!("invalid stored Backend Job result: {error}")))?; + let current_attempt = u8::try_from(current_attempt) + .map_err(|_| Error::Store("Backend Job attempt number is out of range".to_string()))?; + Ok(Some(BackendJobRecord { + workspace_id: workspace_id.to_string(), + request, + intent_fingerprint, + state: BackendJobState::parse(&state)?, + current_attempt, + result, + result_digest, + failure_category, + failure_detail, + created_at, + updated_at, + completed_at, + })) +} + +fn read_backend_job_attempt( + conn: &Connection, + workspace_id: &str, + job_id: &str, + 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", + 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)?, + )), + ).optional()?; + let Some(( + 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, + )) = row + else { + return Ok(None); + }; + let worker = match (runtime_id, worker_id) { + (Some(runtime_id), Some(worker_id)) => Some(RuntimeWorkerRef::new(runtime_id, worker_id)), + (None, None) => None, + _ => { + return Err(Error::Store( + "Backend Job attempt has a partial Worker binding".to_string(), + )); + } + }; + let result = result_json + .map(|value| serde_json::from_str(&value)) + .transpose() + .map_err(|error| { + Error::Store(format!( + "invalid stored Backend Job attempt result: {error}" + )) + })?; + let attempt = u8::try_from(attempt) + .map_err(|_| Error::Store("Backend Job attempt number is out of range".to_string()))?; + Ok(Some(BackendJobAttemptRecord { + workspace_id: workspace_id.to_string(), + job_id: job_id.to_string(), + attempt_id: attempt_id.to_string(), + attempt, + input_revision, + state: BackendJobAttemptState::parse(&state)?, + worker, + runtime_run_id, + dispatched_at, + deadline_at, + result, + result_digest, + failure_category, + failure_detail, + created_at, + updated_at, + completed_at, + })) +} + +fn read_backend_job_delivery( + conn: &Connection, + workspace_id: &str, + delivery_id: &str, +) -> Result> { + let row = conn.query_row( + "SELECT job_id, attempt_id, target_runtime_id, target_worker_id, state, failure_category, failure_detail, created_at, updated_at, delivered_at FROM backend_job_deliveries WHERE workspace_id = ?1 AND delivery_id = ?2", + params![workspace_id, delivery_id], + |row| Ok(( + row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, row.get::<_, String>(3)?, + row.get::<_, String>(4)?, row.get::<_, Option>(5)?, row.get::<_, Option>(6)?, + row.get::<_, String>(7)?, row.get::<_, String>(8)?, row.get::<_, Option>(9)?, + )), + ).optional()?; + let Some(( + job_id, + attempt_id, + runtime_id, + worker_id, + state, + failure_category, + failure_detail, + created_at, + updated_at, + delivered_at, + )) = row + else { + return Ok(None); + }; + Ok(Some(BackendJobDeliveryRecord { + workspace_id: workspace_id.to_string(), + delivery_id: delivery_id.to_string(), + job_id, + attempt_id, + target: RuntimeWorkerRef::new(runtime_id, worker_id), + state: BackendJobDeliveryState::parse(&state)?, + failure_category, + failure_detail, + created_at, + updated_at, + delivered_at, + })) +} + fn worker_registry_select_sql(where_clause: &str) -> String { format!( "SELECT workspace_id, runtime_id, worker_id, display_name, profile, \ @@ -9378,6 +10062,7 @@ fn read_worker_registry_projection( workspace_id: Some(workspace_id.to_string()), display_name: Some(registry.display_name.clone()), profile: registry.profile.clone(), + job: None, workdir_attachments: Vec::new(), } }; @@ -9395,10 +10080,27 @@ fn read_worker_registry_projection( }, ) .transpose()?; + let job = conn + .query_row( + "SELECT attempt.job_id, attempt.attempt_id, job.purpose \ + FROM backend_job_attempts attempt \ + JOIN backend_jobs job ON job.workspace_id = attempt.workspace_id AND job.job_id = attempt.job_id \ + WHERE attempt.workspace_id = ?1 AND attempt.runtime_id = ?2 AND attempt.worker_id = ?3", + params![workspace_id, worker.runtime_id, worker.worker_id], + |row| { + Ok(BackendJobWorkerBinding { + job_id: row.get(0)?, + attempt_id: row.get(1)?, + purpose: row.get(2)?, + }) + }, + ) + .optional()?; Ok(Some(WorkerRegistryProjectionRecord { registry, resource_key, observation, + job, })) } @@ -12308,6 +13010,99 @@ fn migrate_workdir_command_attachment_capabilities_v68_to_v69(conn: &Connection) } } +fn migrate_backend_job_runner_v69_to_v70(conn: &Connection) -> Result<()> { + let current = current_schema_version(conn)?; + if current != 69 { + return Err(Error::Store(format!( + "expected schema version 69 before {BACKEND_JOB_RUNNER_MIGRATION_NAME} migration, found {current}" + ))); + } + let tx = rusqlite::Transaction::new_unchecked(conn, TransactionBehavior::Exclusive)?; + tx.execute_batch( + r#" + CREATE TABLE backend_jobs ( + workspace_id TEXT NOT NULL, + job_id TEXT NOT NULL, + purpose TEXT NOT NULL, + input_revision TEXT NOT NULL, + input_ref TEXT NOT NULL, + request_json TEXT NOT NULL, + intent_fingerprint TEXT NOT NULL, + state TEXT NOT NULL CHECK (state IN ('pending', 'completed', 'failed', 'unknown')), + current_attempt INTEGER NOT NULL CHECK (current_attempt > 0 AND current_attempt <= 3), + result_json TEXT, + result_digest TEXT, + failure_category TEXT, + failure_detail TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + completed_at TEXT, + PRIMARY KEY (workspace_id, job_id), + FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + CHECK ((result_json IS NULL) = (result_digest IS NULL)), + CHECK ((state = 'completed') = (result_json IS NOT NULL)) + ); + CREATE INDEX backend_jobs_active + ON backend_jobs(workspace_id, state, updated_at); + CREATE TABLE backend_job_attempts ( + workspace_id TEXT NOT NULL, + job_id TEXT NOT NULL, + attempt_id TEXT NOT NULL, + attempt INTEGER NOT NULL CHECK (attempt > 0 AND attempt <= 3), + input_revision TEXT NOT NULL, + state TEXT NOT NULL CHECK (state IN ('reserved', 'dispatched', 'completed', 'failed', 'unknown')), + runtime_id TEXT, + worker_id TEXT, + runtime_run_id TEXT, + dispatched_at TEXT, + deadline_at TEXT NOT NULL, + result_json TEXT, + result_digest TEXT, + failure_category TEXT, + failure_detail TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + completed_at TEXT, + PRIMARY KEY (workspace_id, job_id, attempt_id), + UNIQUE (workspace_id, job_id, attempt), + UNIQUE (workspace_id, runtime_id, worker_id), + FOREIGN KEY (workspace_id, job_id) + REFERENCES backend_jobs(workspace_id, job_id) ON DELETE CASCADE, + CHECK ((runtime_id IS NULL) = (worker_id IS NULL)), + CHECK ((result_json IS NULL) = (result_digest IS NULL)), + CHECK ((state = 'completed') = (result_json IS NOT NULL)) + ); + CREATE INDEX backend_job_attempts_recovery + ON backend_job_attempts(workspace_id, state, deadline_at); + CREATE TABLE backend_job_deliveries ( + workspace_id TEXT NOT NULL, + delivery_id TEXT NOT NULL, + job_id TEXT NOT NULL, + attempt_id TEXT NOT NULL, + target_runtime_id TEXT NOT NULL, + target_worker_id TEXT NOT NULL, + state TEXT NOT NULL CHECK (state IN ('pending', 'completed', 'failed')), + failure_category TEXT, + failure_detail TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + delivered_at TEXT, + PRIMARY KEY (workspace_id, delivery_id), + FOREIGN KEY (workspace_id, job_id, attempt_id) + REFERENCES backend_job_attempts(workspace_id, job_id, attempt_id) ON DELETE CASCADE + ); + CREATE INDEX backend_job_deliveries_pending + ON backend_job_deliveries(workspace_id, state, updated_at); + "#, + )?; + tx.execute( + "INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)", + params![70_i64, BACKEND_JOB_RUNNER_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"))?; @@ -13057,7 +13852,10 @@ mod tests { fn downgrade_schema_66_ticket_and_workdir_authority(conn: &Connection) -> Result<()> { conn.execute_batch( - "DROP TABLE typed_ticket_targets; + "DROP TABLE backend_job_deliveries; + DROP TABLE backend_job_attempts; + DROP TABLE backend_jobs; + DROP TABLE typed_ticket_targets; ALTER TABLE typed_tickets ADD COLUMN repository_id TEXT; ALTER TABLE typed_tickets ADD COLUMN ref_selector TEXT; ALTER TABLE worker_workdir_links DROP COLUMN capabilities; @@ -14476,6 +15274,10 @@ mod tests { version: 69, name: WORKDIR_COMMAND_ATTACHMENT_CAPABILITIES_MIGRATION_NAME.to_string(), }, + WorkspaceSchemaMigrationStep { + version: 70, + name: BACKEND_JOB_RUNNER_MIGRATION_NAME.to_string(), + }, ] ); @@ -14546,6 +15348,7 @@ mod tests { 69, WORKDIR_COMMAND_ATTACHMENT_CAPABILITIES_MIGRATION_NAME.to_string(), ), + (70, BACKEND_JOB_RUNNER_MIGRATION_NAME.to_string()), ] ); assert!(!table_exists(conn, "trusted_runtime_records")?); @@ -14762,7 +15565,7 @@ mod tests { .map(|migration| migration.version) .collect::>(), vec![ - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70 ] ); SqliteWorkspaceStore::migrate_database(&path).unwrap(); @@ -14771,7 +15574,7 @@ mod tests { current_schema_version(&conn).unwrap(), LATEST_SCHEMA_VERSION ); - assert_eq!(workspace_schema_migration_history(&conn).unwrap().len(), 20); + assert_eq!(workspace_schema_migration_history(&conn).unwrap().len(), 21); 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()); @@ -17759,6 +18562,7 @@ INSERT INTO worker_registry ( workspace_id: Some("local-dev".to_string()), display_name: Some("Known Worker".to_string()), profile: None, + job: None, workdir_attachments: Vec::new(), }; let orphan = SubscriptionWorker { @@ -17934,6 +18738,7 @@ INSERT INTO worker_registry ( workspace_id: Some("local-dev".to_string()), display_name: None, profile: None, + job: None, workdir_attachments: Vec::new(), }; let commit = store @@ -18318,4 +19123,319 @@ INSERT INTO worker_registry ( None ); } + + #[tokio::test] + async fn backend_jobs_fence_results_project_ownership_and_bound_retries() { + let store = SqliteWorkspaceStore::in_memory().unwrap(); + let workspace_id = "workspace-job"; + store + .upsert_account(&AccountRecord { + account_id: "owner-job".to_string(), + kind: "user".to_string(), + handle: "job-owner".to_string(), + display_name: "Job Owner".to_string(), + created_at: "2026-09-01T00:00:00Z".to_string(), + updated_at: "2026-09-01T00:00:00Z".to_string(), + }) + .unwrap(); + store + .upsert_workspace(&WorkspaceRecord { + workspace_id: workspace_id.to_string(), + owner_account_id: "owner-job".to_string(), + display_name: "Job Workspace".to_string(), + state: "active".to_string(), + created_at: "2026-09-01T00:00:00Z".to_string(), + updated_at: "2026-09-01T00:00:00Z".to_string(), + }) + .await + .unwrap(); + let source = RuntimeWorkerRef::new("embedded-worker-runtime", "source"); + let worker = RuntimeWorkerRef::new("embedded-worker-runtime", "job-worker"); + let retry_worker = RuntimeWorkerRef::new("embedded-worker-runtime", "retry-worker"); + let worker_record = |worker: RuntimeWorkerRef, display_name: &str| WorkerRegistryRecord { + workspace_id: workspace_id.to_string(), + worker, + display_name: display_name.to_string(), + profile: Some("builtin:backend-job".to_string()), + retention_state: "normal".to_string(), + transcript_ref: None, + session_ref: None, + summary_ref: None, + diagnostics_ref: None, + created_at: "2026-09-01T00:00:00Z".to_string(), + updated_at: "2026-09-01T00:00:00Z".to_string(), + }; + store + .upsert_worker_registry(&worker_record(source.clone(), "Source Worker")) + .unwrap(); + store + .upsert_worker_registry(&worker_record(worker.clone(), "Job Worker")) + .unwrap(); + store + .upsert_worker_registry(&worker_record(retry_worker.clone(), "Retry Worker")) + .unwrap(); + store + .upsert_account(&AccountRecord { + account_id: "owner-other".to_string(), + kind: "user".to_string(), + handle: "other-owner".to_string(), + display_name: "Other Owner".to_string(), + created_at: "2026-09-01T00:00:00Z".to_string(), + updated_at: "2026-09-01T00:00:00Z".to_string(), + }) + .unwrap(); + store + .upsert_workspace(&WorkspaceRecord { + workspace_id: "workspace-other".to_string(), + owner_account_id: "owner-other".to_string(), + display_name: "Other Workspace".to_string(), + state: "active".to_string(), + created_at: "2026-09-01T00:00:00Z".to_string(), + updated_at: "2026-09-01T00:00:00Z".to_string(), + }) + .await + .unwrap(); + let cross_workspace_target = + RuntimeWorkerRef::new("embedded-worker-runtime", "cross-workspace-target"); + let mut cross_workspace_record = + worker_record(cross_workspace_target.clone(), "Cross Workspace Target"); + cross_workspace_record.workspace_id = "workspace-other".to_string(); + store + .upsert_worker_registry(&cross_workspace_record) + .unwrap(); + let request = BackendJobRequest { + job_id: "check:T-1:r1".to_string(), + purpose: "ticket_item_check".to_string(), + input_revision: "1".to_string(), + input_ref: "ticket://T-1/revisions/1".to_string(), + input: serde_json::json!({"title": "check"}), + instruction: "Check the immutable input.".to_string(), + profile: "builtin:backend-job".to_string(), + source_worker: Some(source.clone()), + notification_target: Some(source.clone()), + limits: crate::backend_job::BackendJobLimits { + max_attempts: 2, + ..Default::default() + }, + }; + + let mut cross_workspace_notification = request.clone(); + cross_workspace_notification.job_id = "cross-workspace-notification".to_string(); + cross_workspace_notification.notification_target = Some(cross_workspace_target); + assert!( + store + .reserve_backend_job( + workspace_id, + &cross_workspace_notification, + "2026-09-01T00:00:00Z", + ) + .is_err(), + "notification intent must not target a Worker from another Workspace" + ); + assert!( + store + .accept_backend_job_result( + "workspace-other", + &worker, + &BackendJobResultSubmission { + job_id: request.job_id.clone(), + attempt_id: attempt_id(&request.job_id, 1), + input_revision: request.input_revision.clone(), + result: serde_json::json!({"cross_workspace": true}), + }, + "2026-09-01T00:00:00Z", + ) + .is_err(), + "result acceptance must be scoped to the Job Workspace" + ); + + let first = store + .reserve_backend_job(workspace_id, &request, "2026-09-01T00:00:00Z") + .unwrap(); + let replay = store + .reserve_backend_job(workspace_id, &request, "2026-09-01T00:00:01Z") + .unwrap(); + assert!(replay.replayed); + assert_eq!(replay.attempt.attempt_id, first.attempt.attempt_id); + store + .bind_backend_job_attempt_worker( + workspace_id, + &request.job_id, + &first.attempt.attempt_id, + &worker, + Some("runtime-run-1"), + "2026-09-01T00:00:02Z", + ) + .unwrap(); + assert_eq!( + store + .worker_registry_projection(workspace_id, &worker) + .unwrap() + .unwrap() + .job, + Some(BackendJobWorkerBinding { + job_id: request.job_id.clone(), + attempt_id: first.attempt.attempt_id.clone(), + purpose: request.purpose.clone(), + }) + ); + let submission = BackendJobResultSubmission { + job_id: request.job_id.clone(), + attempt_id: first.attempt.attempt_id.clone(), + input_revision: request.input_revision.clone(), + result: serde_json::json!({"valid": true}), + }; + assert!( + store + .accept_backend_job_result( + workspace_id, + &RuntimeWorkerRef::new("embedded-worker-runtime", "wrong"), + &submission, + "2026-09-01T00:00:03Z", + ) + .is_err() + ); + let accepted = store + .accept_backend_job_result(workspace_id, &worker, &submission, "2026-09-01T00:00:03Z") + .unwrap(); + assert_eq!(accepted.job.state, BackendJobState::Completed); + assert!( + store + .accept_backend_job_result( + workspace_id, + &worker, + &submission, + "2026-09-01T00:00:04Z", + ) + .unwrap() + .replayed + ); + assert!( + store + .accept_backend_job_result( + workspace_id, + &worker, + &BackendJobResultSubmission { + result: serde_json::json!({"valid": false}), + ..submission.clone() + }, + "2026-09-01T00:00:04Z", + ) + .is_err() + ); + + // Missing notification targets are durable delivery failures and do + // not alter the already-committed Job result. + store + .reserve_backend_job_delivery( + workspace_id, + "delivery-1", + &request.job_id, + &source, + "2026-09-01T00:00:05Z", + ) + .unwrap(); + store + .finish_backend_job_delivery( + workspace_id, + "delivery-1", + BackendJobDeliveryState::Failed, + Some("target_missing"), + Some("target is gone"), + "2026-09-01T00:00:06Z", + ) + .unwrap(); + assert_eq!( + store + .get_backend_job(workspace_id, &request.job_id) + .unwrap() + .unwrap() + .state, + BackendJobState::Completed + ); + assert!( + !store + .delete_worker_registry(workspace_id, &worker) + .unwrap() + .changes + .is_empty(), + "Job result retention must not prevent canonical Worker removal" + ); + assert_eq!( + store + .get_backend_job(workspace_id, &request.job_id) + .unwrap() + .unwrap() + .result, + Some(serde_json::json!({"valid": true})), + "Worker removal must not discard the durable Job result" + ); + + let mut retry_request = request; + retry_request.job_id = "bounded-retry".to_string(); + retry_request.notification_target = None; + let failed = store + .reserve_backend_job(workspace_id, &retry_request, "2026-09-01T00:01:00Z") + .unwrap(); + store + .bind_backend_job_attempt_worker( + workspace_id, + &retry_request.job_id, + &failed.attempt.attempt_id, + &retry_worker, + None, + "2026-09-01T00:01:01Z", + ) + .unwrap(); + store + .finish_backend_job_attempt( + workspace_id, + &retry_request.job_id, + &failed.attempt.attempt_id, + BackendJobAttemptState::Failed, + "timeout", + "deadline exceeded", + "2026-09-01T00:01:02Z", + ) + .unwrap(); + assert!( + store + .accept_backend_job_result( + workspace_id, + &retry_worker, + &BackendJobResultSubmission { + job_id: retry_request.job_id.clone(), + attempt_id: failed.attempt.attempt_id.clone(), + input_revision: retry_request.input_revision.clone(), + result: serde_json::json!({"late": true}), + }, + "2026-09-01T00:01:03Z", + ) + .is_err() + ); + let second = store + .reserve_backend_job_retry(workspace_id, &retry_request.job_id, "2026-09-01T00:01:04Z") + .unwrap(); + assert_eq!(second.attempt.attempt, 2); + store + .finish_backend_job_attempt( + workspace_id, + &retry_request.job_id, + &second.attempt.attempt_id, + BackendJobAttemptState::Unknown, + "worker_unknown", + "worker unavailable", + "2026-09-01T00:01:05Z", + ) + .unwrap(); + assert!( + store + .reserve_backend_job_retry( + workspace_id, + &retry_request.job_id, + "2026-09-01T00:01:06Z", + ) + .is_err() + ); + } } diff --git a/crates/workspace-server/src/workspace_deletion.rs b/crates/workspace-server/src/workspace_deletion.rs index fb6fe3f03..797fc7a69 100644 --- a/crates/workspace-server/src/workspace_deletion.rs +++ b/crates/workspace-server/src/workspace_deletion.rs @@ -18,6 +18,9 @@ use crate::{Error, Result}; const WORKSPACE_DELETION_PURGE_TABLES: &[&str] = &[ "artifacts", "audit_events", + "backend_job_deliveries", + "backend_job_attempts", + "backend_jobs", "external_workdir_grants", "flow_source_revisions", "flow_sources", diff --git a/crates/workspace-server/src/workspace_subscription.rs b/crates/workspace-server/src/workspace_subscription.rs index 86772e6ea..afd4fbfbe 100644 --- a/crates/workspace-server/src/workspace_subscription.rs +++ b/crates/workspace-server/src/workspace_subscription.rs @@ -449,6 +449,7 @@ fn project_registry_worker( workspace_id: Some(record.registry.workspace_id.clone()), display_name: Some(record.registry.display_name.clone()), profile: record.registry.profile.clone(), + job: None, workdir_attachments: Vec::new(), } }; @@ -457,6 +458,13 @@ fn project_registry_worker( worker.workspace_id = Some(record.registry.workspace_id); worker.display_name = Some(record.registry.display_name); worker.profile = record.registry.profile; + worker.job = record + .job + .map(|job| protocol::subscription::SubscriptionWorkerJob { + job_id: job.job_id, + attempt_id: job.attempt_id, + purpose: job.purpose, + }); if !project_working_directory(api, &record.registry.worker, &mut worker) { return None; } @@ -616,6 +624,7 @@ mod tests { workspace_id: Some("workspace-1".to_string()), display_name: Some("Companion".to_string()), profile: Some("builtin:companion".to_string()), + job: None, workdir_attachments: vec![ protocol::subscription::SubscriptionWorkerWorkdirAttachment { alias: "external".to_string(), diff --git a/docs/design/durable-operations.md b/docs/design/durable-operations.md index 315e93a64..39269d87f 100644 --- a/docs/design/durable-operations.md +++ b/docs/design/durable-operations.md @@ -301,6 +301,55 @@ The public result contains only the Workdir ID, failure category. Internal operation identifiers, checkpoints, provider paths, and credentials are not public DTO fields. +## Backend-owned bounded Jobs + +A Backend Job is durable side-effect intent whose model execution is delegated to +one ordinary Runtime-managed Worker per attempt. The Workspace Server owns the +Job; `source_worker` and `notification_target` are provenance and delivery +addresses, not parent ownership. Stopping or losing either Worker does not revoke +Job intent. Attempt records retain the bound Worker identity as historical +evidence without foreign-keying Worker retention to Job retention; normal Worker +stop, archive, pin/hold, and removal policy remains authoritative. + +The durable split is deliberate: + +- `backend_jobs` stores immutable bounded input, its fingerprint, the current + attempt number, and the final structured result or terminal failure; +- `backend_job_attempts` binds one attempt and input revision to one dedicated + `(Workspace, Runtime, Worker)` identity, optional Runtime run evidence, a + deadline, and its bounded outcome; +- `backend_job_deliveries` records best-effort notification separately from the + result, so a missing or stopped recipient never discards a result or reruns + model work. + +Exact dispatch retry reuses the deterministic attempt and Worker allocation key. +Only explicit re-evaluation increments the bounded attempt number. The runner +uses the existing embedded Runtime spawn, Worker registry, Session input, +observation, usage, and lifecycle paths with no Workdir attachment. It uses the +dedicated `builtin:backend-job` profile, whose tools cannot mutate Workspace +state, recursively create Workers or Jobs, or inherit caller Workdir authority. + +Success is accepted only by the structured result boundary. The submission is +fenced by Workspace, Job ID, attempt ID, input revision, and the bound Runtime +Worker identity, and its JSON bytes are bounded before persistence. Worker final +prose, Idle, Stopped, or a successful model call are not success evidence. +Identical structured submission is idempotent; conflicting replay, stale or late +attempts, wrong revisions, and other Workers fail closed. + +On restart, an unbound reservation is redispatched with its stable allocation. +A dispatched attempt continues only when its bound Worker remains observable; +otherwise it becomes `unknown`. Durable deadlines fence acceptance before the +runner requests Worker stop. The design does not claim general exactly-once +model execution: a crash between Runtime creation and durable binding converges +through stable allocation where available, and unrecoverable external evidence +is surfaced as `unknown` rather than guessed. Historical attempts preserve the +reason for retry without duplicating the Runtime Worker lifecycle state machine. + +Workspace Worker subscriptions expose an explicit Job binding (`job_id`, +`attempt_id`, and `purpose`) in snapshot and live projections. Sidebar hiding is +a Web View policy over that canonical metadata; the Worker remains registered, +observable, and eligible for its canonical Console route. + ## Diagnostics and audit Persist bounded error categories and identifiers needed to investigate or retry. diff --git a/resources/profiles/backend-job.dcdl b/resources/profiles/backend-job.dcdl new file mode 100644 index 000000000..15c64d83c --- /dev/null +++ b/resources/profiles/backend-job.dcdl @@ -0,0 +1,33 @@ +import "./base.dcdl" // { + slug = "backend-job"; + description = "Backend-owned bounded Job Worker profile."; + scope = "workspace_read"; + model = { ref = "codex-oauth/gpt-5.6-luna"; }; + engine = { + instruction = "internal.backend_job_system"; + reasoning = "medium"; + max_tokens = 8192; + }; + + feature = { + task = { enabled = false; }; + memory = { enabled = false; }; + web = { enabled = false; }; + image = { enabled = false; }; + sub_worker = { enabled = false; }; + flow = { enabled = false; }; + worker = { enabled = false; direct_spawn = false; }; + workspace_worker_discovery = { enabled = false; }; + objective = { enabled = false; }; + manage_workdir = { enabled = false; }; + ticket = { enabled = false; authoring = false; thread = false; intake = false; }; + merge_request = { + show = false; + open = false; + review = false; + readiness_check = false; + complete = false; + }; + orchestration = { enabled = false; }; + }; +} diff --git a/resources/prompts/catalog.dcdl b/resources/prompts/catalog.dcdl index 0f7a2a909..01f491867 100644 --- a/resources/prompts/catalog.dcdl +++ b/resources/prompts/catalog.dcdl @@ -20,6 +20,7 @@ roleOrchestrator = import "./role/orchestrator.md"; roleReviewer = import "./role/reviewer.md"; internalCompactSystem = import "./internal/compact_system.md"; internalFlowVerifierSystem = import "./internal/flow_verifier_system.md"; +internalBackendJobSystem = import "./internal/backend_job_system.md"; internalMemoryConsolidationSystem = import "./internal/memory_consolidation_system.md"; internalMemoryExtractSystem = import "./internal/memory_extract_system.md"; internalWorkspaceOrchestratorQueueAttention = import "./internal/workspace_orchestrator_queue_attention.md"; @@ -55,6 +56,7 @@ in internal = { compact_system = internalCompactSystem.content; flow_verifier_system = internalFlowVerifierSystem.content; + backend_job_system = internalBackendJobSystem.content; memory_consolidation_system = internalMemoryConsolidationSystem.content; memory_extract_system = internalMemoryExtractSystem.content; workspace_orchestrator_queue_attention = internalWorkspaceOrchestratorQueueAttention.content; diff --git a/resources/prompts/internal/backend_job_system.md b/resources/prompts/internal/backend_job_system.md new file mode 100644 index 000000000..832736534 --- /dev/null +++ b/resources/prompts/internal/backend_job_system.md @@ -0,0 +1,5 @@ +You are a Backend-owned bounded Job Worker. Execute only the immutable Job envelope in the first user message. + +You are not a child of the Worker recorded as provenance or notification target. Do not create or mutate Tickets, Objectives, Workers, Workdirs, Merge Requests, Memory, or other Workspace state. Do not recursively create Jobs. + +A normal assistant response, Worker Idle state, or Worker stop is not Job success. Call `SubmitBackendJobResult` exactly once with the bounded structured result. If that capability rejects the result, explain the failure in final prose without claiming success. diff --git a/web/workspace/src/lib/generated/protocol-validator.ts b/web/workspace/src/lib/generated/protocol-validator.ts index 34f22a4da..e3f57a918 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"},"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":"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":{"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,"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 the TUI when a session starts empty.\n\nBuilt once in the Worker controller from the resolved manifest and\ntransmitted alongside `Event::Snapshot` so clients don't need\ntheir own view of the manifest.","properties":{"context_tokens":{"default":0,"description":"Estimated current session context tokens at connect time.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"context_window":{"default":0,"description":"Model context window in tokens. Always filled by the Worker greeting.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"cwd":{"type":"string"},"model":{"type":"string"},"provider":{"type":"string"},"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"]},"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"},"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"},"RunResult":{"oneOf":[{"enum":["finished","paused","limit_reached"],"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"}]},"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,"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_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"},"subject_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","subject_revision","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"},"snapshot_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id","selector","snapshot_revision","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"},"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."},"subject_revision":{"description":"Producer-owned monotonic revision for this Worker subject.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"workdir_attachments":{"items":{"$ref":"#/$defs/SubscriptionWorkerWorkdirAttachment"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"},"worker_state":{"anyOf":[{"$ref":"#/$defs/WorkerStateSnapshot"},{"type":"null"}],"description":"Latest revisioned 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","subject_revision","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"},"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"},"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":"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":{"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,"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 the TUI when a session starts empty.\n\nBuilt once in the Worker controller from the resolved manifest and\ntransmitted alongside `Event::Snapshot` so clients don't need\ntheir own view of the manifest.","properties":{"context_tokens":{"default":0,"description":"Estimated current session context tokens at connect time.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"context_window":{"default":0,"description":"Model context window in tokens. Always filled by the Worker greeting.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"cwd":{"type":"string"},"model":{"type":"string"},"provider":{"type":"string"},"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"]},"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"},"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"},"RunResult":{"oneOf":[{"enum":["finished","paused","limit_reached"],"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"}]},"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,"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_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"},"subject_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","subject_revision","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"},"snapshot_revision":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id","selector","snapshot_revision","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."},"subject_revision":{"description":"Producer-owned monotonic revision for this Worker subject.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"workdir_attachments":{"items":{"$ref":"#/$defs/SubscriptionWorkerWorkdirAttachment"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"},"worker_state":{"anyOf":[{"$ref":"#/$defs/WorkerStateSnapshot"},{"type":"null"}],"description":"Latest revisioned 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","subject_revision","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"}; export const RUST_SERIALIZED_SUBSCRIPTION_FRAME_FIXTURES: readonly unknown[] = [{"protocol_version":1,"frame":"response","message":{"result":"subscribed","payload":{"request_id":"fixture-request","subscription_id":"fixture-subscription","selector":{"topic":"workspace_workers"},"snapshot_revision":1,"snapshot":{"topic":"workers","data":{"workers":[{"worker_id":"fixture-worker","runtime_id":"fixture-runtime","resource_key":"fixture-worker-resource","availability":"observed","subject_revision":1,"state":"idle","has_running_internal_workers":false,"workspace_id":"fixture-workspace","workdir_attachments":[{"alias":"external","working_directory_id":"fixture-external-workdir"}]}]}}}}},{"protocol_version":1,"frame":"event","message":{"event":"event","data":{"subscription_id":"fixture-subscription","subject_revision":2,"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":[]}}}}}}}}]; const utf8 = new TextEncoder(); diff --git a/web/workspace/src/lib/generated/protocol.ts b/web/workspace/src/lib/generated/protocol.ts index 5f1a8d336..c4cd75815 100644 --- a/web/workspace/src/lib/generated/protocol.ts +++ b/web/workspace/src/lib/generated/protocol.ts @@ -279,6 +279,8 @@ export type SubscriptionWorkerWorkdirAttachment = { */ alias: string, repository_key?: string | null, working_directory_id: SubscriptionWorkdirId, }; +export type SubscriptionWorkerJob = { job_id: string, attempt_id: string, purpose: string, }; + export type SubscriptionWorker = { worker_id: SubscriptionWorkerId, /** * Set by the Workspace Server when projecting a Runtime-owned Worker to clients. @@ -307,7 +309,12 @@ worker_state?: WorkerStateSnapshot | null, /** * Runtime catalog lifecycle compatibility projection; not foreground-state authority. */ -state: SubscriptionWorkerState, has_running_internal_workers: boolean, workspace_id?: string | null, display_name?: string | null, profile?: string | null, workdir_attachments?: Array, }; +state: SubscriptionWorkerState, has_running_internal_workers: boolean, workspace_id?: string | null, display_name?: string | null, profile?: string | null, +/** + * Backend-owned Job binding. Runtime producers leave this unset; the + * Workspace projection enriches both snapshots and live updates. + */ +job?: SubscriptionWorkerJob | null, workdir_attachments?: Array, }; export type WorkspaceSubscriptionWorkdir = { working_directory_id: SubscriptionWorkdirId, repository_key: string, state: string, }; diff --git a/web/workspace/src/lib/workspace/sidebar/types.ts b/web/workspace/src/lib/workspace/sidebar/types.ts index 8e68ed3bf..a6d914954 100644 --- a/web/workspace/src/lib/workspace/sidebar/types.ts +++ b/web/workspace/src/lib/workspace/sidebar/types.ts @@ -30,6 +30,7 @@ import type { Event as PodProtocolEvent, Method as PodProtocolMethod, Segment as PodProtocolSegment, + SubscriptionWorkerJob, WorkerStateSnapshot, } from "$lib/generated/protocol"; import type { @@ -88,6 +89,7 @@ export type Worker = display_name: string; tags: string[]; worker_state?: WorkerStateSnapshot | null; + job?: SubscriptionWorkerJob | null; diagnostics: Diagnostic[]; }; diff --git a/web/workspace/src/lib/workspace/sidebar/worker-subscription.test.ts b/web/workspace/src/lib/workspace/sidebar/worker-subscription.test.ts index 14cf3dc12..9ed0e52b4 100644 --- a/web/workspace/src/lib/workspace/sidebar/worker-subscription.test.ts +++ b/web/workspace/src/lib/workspace/sidebar/worker-subscription.test.ts @@ -88,6 +88,47 @@ Deno.test('workspace Worker snapshot keeps equal local ids from different Runtim assertEquals([...projection.workers.keys()].sort(), ['runtime-a:1', 'runtime-b:1']); }); +Deno.test('workspace Worker snapshot and live update preserve canonical Job metadata', () => { + const projection = createWorkspaceWorkersProjection(); + const snapshotWorker = worker('runtime-a', 'job-worker', 1); + snapshotWorker.job = { + job_id: 'check:T-1:r1', + attempt_id: 'check:T-1:r1:attempt:1', + purpose: 'ticket_item_check', + }; + applyWorkspaceWorkersFrame(projection, { + protocol_version: 1, + frame: 'response', + message: { + result: 'subscribed', + payload: { + request_id: 'request-1', + subscription_id: 'subscription-1', + selector: { topic: 'workspace_workers' }, + snapshot_revision: 1, + snapshot: { topic: 'workers', data: { workers: [snapshotWorker] } }, + }, + }, + }); + assertEquals(projection.workers.get('runtime-a:job-worker')?.job, snapshotWorker.job); + + const updated = { ...snapshotWorker, subject_revision: 2 }; + updated.job = { ...snapshotWorker.job, attempt_id: 'check:T-1:r1:attempt:2' }; + applyWorkspaceWorkersFrame(projection, { + protocol_version: 1, + frame: 'event', + message: { + event: 'event', + data: { + subscription_id: 'subscription-1', + subject_revision: 2, + payload: { event: 'worker_upserted', data: { worker: updated } }, + }, + }, + }); + assertEquals(projection.workers.get('runtime-a:job-worker')?.job, updated.job); +}); + Deno.test('workspace Worker reducer ignores stale events and removes composite subject', () => { const projection = createWorkspaceWorkersProjection(); projection.workers.set('runtime-a:1', worker('runtime-a', '1', 3)); diff --git a/web/workspace/src/lib/workspace/sidebar/worker-subscription.ts b/web/workspace/src/lib/workspace/sidebar/worker-subscription.ts index 8f957e9f2..7b419a563 100644 --- a/web/workspace/src/lib/workspace/sidebar/worker-subscription.ts +++ b/web/workspace/src/lib/workspace/sidebar/worker-subscription.ts @@ -143,6 +143,7 @@ function projectWorker( display_name: displayName, label: displayName, profile: worker.profile ?? null, + job: worker.job ?? null, tags: [], workspace: { visibility: "workspace", diff --git a/web/workspace/src/lib/workspace/sidebar/workers.test.ts b/web/workspace/src/lib/workspace/sidebar/workers.test.ts index 13020c01e..f1d917306 100644 --- a/web/workspace/src/lib/workspace/sidebar/workers.test.ts +++ b/web/workspace/src/lib/workspace/sidebar/workers.test.ts @@ -63,6 +63,19 @@ Deno.test("live runtime workers are sidebar targets and console targets", () => assertEquals(canOpenWorkerConsole(liveWorker), true); }); +Deno.test("Backend Job Workers are hidden only from the sidebar View", () => { + const jobWorker = worker({ + state: "running", + job: { + job_id: "check:T-1:r1", + attempt_id: "check:T-1:r1:attempt:1", + purpose: "ticket_item_check", + }, + }); + assertEquals(canShowWorkerInSidebar(jobWorker), false); + assertEquals(canOpenWorkerConsole(jobWorker), true); +}); + Deno.test("sidebar workers sort running then idle then stopped", () => { const workers = [ worker({ worker_id: "3", display_name: "Stopped", state: "stopped" }), diff --git a/web/workspace/src/lib/workspace/sidebar/workers.ts b/web/workspace/src/lib/workspace/sidebar/workers.ts index f648540d2..42c3e9098 100644 --- a/web/workspace/src/lib/workspace/sidebar/workers.ts +++ b/web/workspace/src/lib/workspace/sidebar/workers.ts @@ -22,15 +22,15 @@ export function sidebarWorkerActivity( } export function canShowWorkerInSidebar( - worker: Pick, + worker: Pick, ): boolean { - return worker.implementation.kind !== 'backend_worker_registry'; + return !worker.job && worker.implementation.kind !== 'backend_worker_registry'; } export function canOpenWorkerConsole( worker: Pick, ): boolean { - return canShowWorkerInSidebar(worker); + return worker.implementation.kind !== 'backend_worker_registry'; } type SidebarWorkerLink = Pick;