From a2cec5afd1790119ee191298d2d2e2acba305ab0 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 16 Oct 2026 05:28:17 +0900 Subject: [PATCH] fix: preserve console alert order and improve alert colors --- crates/protocol/src/lib.rs | 54 ++ crates/protocol/src/typescript.rs | 27 +- crates/standalone/src/host.rs | 5 + crates/tui/src/app.rs | 3 + crates/worker-runtime/src/http_server.rs | 1 + crates/worker-runtime/src/runtime.rs | 1 + crates/worker-runtime/src/worker_backend.rs | 92 ++- crates/worker/src/discovery.rs | 4 + crates/worker/src/in_flight.rs | 156 +++- crates/worker/src/ipc/alerter.rs | 388 +++++++++- crates/worker/src/ipc/protocol_session.rs | 4 +- crates/worker/src/ipc/server.rs | 5 + crates/worker/src/segment_log_sink.rs | 139 +++- crates/worker/src/spawn/comm_tools.rs | 2 + crates/worker/src/worker.rs | 80 +- crates/workspace-server/src/server.rs | 1 + web/workspace/deno.json | 2 +- .../src/lib/generated/protocol-validator.ts | 2 +- web/workspace/src/lib/generated/protocol.ts | 14 +- .../console/ConsoleLineItem.browser.spec.ts | 93 ++- .../workspace/console/ConsoleLineItem.svelte | 18 + .../lib/workspace/console/alert-order.test.ts | 687 ++++++++++++++++++ .../src/lib/workspace/console/alert-order.ts | 131 ++++ .../src/lib/workspace/console/model.ts | 230 +++++- .../console/console-display.browser.spec.ts | 49 ++ 25 files changed, 2098 insertions(+), 90 deletions(-) create mode 100644 web/workspace/src/lib/workspace/console/alert-order.test.ts create mode 100644 web/workspace/src/lib/workspace/console/alert-order.ts diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 9bd93d407..f7baece98 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -1497,6 +1497,11 @@ pub enum Event { #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] pub struct Alert { + /// Stable only within the emitting live Worker; not durable alert history. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub position: Option, pub level: AlertLevel, pub source: AlertSource, pub message: String, @@ -1504,6 +1509,18 @@ pub struct Alert { pub timestamp_ms: i64, } +/// Display anchor captured at emission, resolved when pending text is committed. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum AlertPosition { + Start, + AfterEntry { entry_id: String }, + AfterToolCall { call_id: String }, + AfterInFlight { block_id: u64 }, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] @@ -1704,6 +1721,9 @@ pub struct InFlightCompaction { #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] pub struct InFlightSnapshot { + /// Live-Worker identities parallel to the blocks actually included below. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub block_ids: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub blocks: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -2721,6 +2741,7 @@ mod tests { }, state: WorkerStatus::Running.into(), in_flight: InFlightSnapshot { + block_ids: Vec::new(), blocks: vec![ InFlightBlock::Text { text: "hel".into(), @@ -3048,9 +3069,42 @@ mod tests { } } + #[test] + fn alert_ordering_metadata_is_optional_and_uses_tagged_wire_anchors() { + let legacy = serde_json::json!({ "level": "warn", "source": "worker", "message": "old", "timestamp_ms": 42 }); + let alert: Alert = serde_json::from_value(legacy.clone()).unwrap(); + assert!(alert.id.is_none()); + assert!(alert.position.is_none()); + assert_eq!(serde_json::to_value(alert).unwrap(), legacy); + for position in [ + serde_json::json!({ "kind": "start" }), + serde_json::json!({ "kind": "after_entry", "entry_id": "entry-1" }), + serde_json::json!({ "kind": "after_tool_call", "call_id": "call-1" }), + serde_json::json!({ "kind": "after_in_flight", "block_id": 7 }), + ] { + let mut wire = legacy.clone(); + wire["id"] = serde_json::json!("alert-1"); + wire["position"] = position; + let alert: Alert = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(alert).unwrap(), wire); + } + let old_snapshot: InFlightSnapshot = serde_json::from_value(serde_json::json!({})).unwrap(); + assert!(old_snapshot.block_ids.is_empty()); + assert_eq!( + serde_json::to_value(old_snapshot).unwrap(), + serde_json::json!({}) + ); + let wire = + serde_json::json!({"blocks": [{"kind": "text", "text": "pending"}], "block_ids": [7]}); + let snapshot: InFlightSnapshot = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(snapshot).unwrap(), wire); + } + #[test] fn event_alert_format() { let event = Event::Alert(Alert { + id: None, + position: None, level: AlertLevel::Warn, source: AlertSource::Compactor, message: "compaction failed".into(), diff --git a/crates/protocol/src/typescript.rs b/crates/protocol/src/typescript.rs index 458c1c257..d2fc61d91 100644 --- a/crates/protocol/src/typescript.rs +++ b/crates/protocol/src/typescript.rs @@ -4,19 +4,19 @@ use serde_json::{Map, Value}; use ts_rs::{Config, TS}; use crate::{ - Alert, AlertLevel, AlertSource, CommandEvent, CommandSnapshot, CommandStatus, CommandStream, - CommandStreamSlice, CompactionLifecycle, CompactionLifecycleState, CompactionPhase, - CompactionTrigger, CompletionContext, CompletionEntry, CompletionKind, ContextTokenSource, - ContextUsage, ErrorCode, Event, FeatureInvocation, FeatureInvocationDescriptor, - FeatureInvocationIdentity, FeatureInvocationResult, FeatureInvocationStatus, - FeatureInvocationSyntax, Greeting, InFlightBlock, InFlightCompaction, InFlightSnapshot, - InFlightToolCallState, InternalWorkerKind, InternalWorkerRef, InternalWorkerSnapshot, - InvocationArgumentDescriptor, InvocationArgumentType, InvocationArgumentValue, - InvocationClientAdapter, InvocationCompletion, InvocationValue, InvokeKind, MemoryWorkerEvent, - Method, PasteArtifactAvailability, PasteArtifactMediaType, PasteArtifactRef, - PendingSubmissionSummary, PendingSubmissionsSnapshot, Permission, ReasoningConfig, - RewindSummary, RunFailureKind, RunResult, RunResumeSource, RunYieldReason, ScopeRule, Segment, - SessionContentPart, SessionConversationTurn, SessionEntryProvenance, + Alert, AlertLevel, AlertPosition, AlertSource, CommandEvent, CommandSnapshot, CommandStatus, + CommandStream, CommandStreamSlice, CompactionLifecycle, CompactionLifecycleState, + CompactionPhase, CompactionTrigger, CompletionContext, CompletionEntry, CompletionKind, + ContextTokenSource, ContextUsage, ErrorCode, Event, FeatureInvocation, + FeatureInvocationDescriptor, FeatureInvocationIdentity, FeatureInvocationResult, + FeatureInvocationStatus, FeatureInvocationSyntax, Greeting, InFlightBlock, InFlightCompaction, + InFlightSnapshot, InFlightToolCallState, InternalWorkerKind, InternalWorkerRef, + InternalWorkerSnapshot, InvocationArgumentDescriptor, InvocationArgumentType, + InvocationArgumentValue, InvocationClientAdapter, InvocationCompletion, InvocationValue, + InvokeKind, MemoryWorkerEvent, Method, PasteArtifactAvailability, PasteArtifactMediaType, + PasteArtifactRef, PendingSubmissionSummary, PendingSubmissionsSnapshot, Permission, + ReasoningConfig, RewindSummary, RunFailureKind, RunResult, RunResumeSource, RunYieldReason, + ScopeRule, Segment, SessionContentPart, SessionConversationTurn, SessionEntryProvenance, SessionHistoryLineageBoundary, SessionHistoryPage, SessionMessageRole, SessionSnapshot, SessionSnapshotEntry, SessionSnapshotEntryData, SessionToolAttachment, SubmissionDisposition, SymlinkPolicy, ToolResultDisposition, TurnResult, UploadedFileAvailability, UploadedFileRef, @@ -65,6 +65,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); diff --git a/crates/standalone/src/host.rs b/crates/standalone/src/host.rs index 85ee39453..ba1f8a39c 100644 --- a/crates/standalone/src/host.rs +++ b/crates/standalone/src/host.rs @@ -619,6 +619,11 @@ async fn run_protocol_session( event = events.recv() => { match event { Ok(event) => { + let Ok(event) = handle.alerter.freshen_event(event) else { + // The retained anchor was evicted; reconnect must obtain + // a paired session/Alert snapshot rather than a stale row. + return; + }; if !send_protocol_event(&peer, event).await { return; } diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 5698d7d67..b35921be3 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -4319,6 +4319,8 @@ mod completion_flow_tests { let mut app = App::new("test".into()); let message = "mid-run compaction failed: summary unavailable"; app.handle_worker_event(Event::Alert(protocol::Alert { + id: None, + position: None, level: AlertLevel::Error, source: AlertSource::Compactor, message: message.into(), @@ -4402,6 +4404,7 @@ mod completion_flow_tests { }, state: test_worker_state(WorkerStatus::Running), in_flight: InFlightSnapshot { + block_ids: Vec::new(), blocks: vec![ InFlightBlock::Thinking { text: "why".into(), diff --git a/crates/worker-runtime/src/http_server.rs b/crates/worker-runtime/src/http_server.rs index b635a4e75..ff0c36ea9 100644 --- a/crates/worker-runtime/src/http_server.rs +++ b/crates/worker-runtime/src/http_server.rs @@ -4821,6 +4821,7 @@ mod ws_tests { }, state: protocol::WorkerStateSnapshot::initial(), in_flight: protocol::InFlightSnapshot { + block_ids: Vec::new(), blocks: Vec::new(), commands: Vec::new(), compaction: None, diff --git a/crates/worker-runtime/src/runtime.rs b/crates/worker-runtime/src/runtime.rs index 6f97ce2e5..12c45950c 100644 --- a/crates/worker-runtime/src/runtime.rs +++ b/crates/worker-runtime/src/runtime.rs @@ -11818,6 +11818,7 @@ mod tests { }, state: protocol::WorkerStatus::Running.into(), in_flight: protocol::InFlightSnapshot { + block_ids: Vec::new(), blocks: Vec::new(), commands: Vec::new(), compaction: None, diff --git a/crates/worker-runtime/src/worker_backend.rs b/crates/worker-runtime/src/worker_backend.rs index feedf3526..4df6ddf76 100644 --- a/crates/worker-runtime/src/worker_backend.rs +++ b/crates/worker-runtime/src/worker_backend.rs @@ -2901,12 +2901,33 @@ where let mut events = streams.events; let mut entry_events = streams.log_entries; let bridge_worker_state = worker_state.clone(); + let bridge_handle = handle.clone(); let bridge_task = match self.spawn_on_adapter_runtime(async move { loop { tokio::select! { event = events.recv() => { match event { - Ok(mut event) => { + Ok(event) => { + let mut event = match bridge_handle.alerter.freshen_event(event) { + Ok(event) => event, + Err(_) => { + // This shared bridge also carries Worker state: do + // not terminate it when a bounded Alert was evicted. + // Replace both receivers with the paired snapshot. + let replacement = subscribe_worker_protocol_session(&bridge_handle); + events = replacement.events; + entry_events = replacement.log_entries; + let mut snapshot = replacement.snapshot_event; + if apply_protocol_worker_state(&bridge_worker_state, &mut snapshot).is_err() { + break; + } + let _ = bridge_context.publish_protocol_event(snapshot); + for alert in replacement.alert_snapshot { + let _ = bridge_context.publish_protocol_event(Event::Alert(alert)); + } + continue; + } + }; match apply_protocol_worker_state(&bridge_worker_state, &mut event) { Ok(true) => { let _ = bridge_context.publish_protocol_event(event); @@ -4525,6 +4546,10 @@ where }, }; if let Some(event) = event { + let Ok(event) = relay_handle.alerter.freshen_event(event) else { + // An evicted Alert needs a fresh paired snapshot. + break; + }; tokio::select! { biased; _ = relay_handle.protocol_closed() => break, @@ -5177,6 +5202,71 @@ mod tests { assert!(backend.stop_worker(&request.worker_ref).is_accepted()); } + #[cfg(feature = "ws-server")] + #[test] + #[serial_test::serial(worker_allocation)] + fn protocol_transport_reconnects_instead_of_forwarding_an_evicted_alert() { + let root = tempfile::tempdir().unwrap(); + let runtime_store = root.path().join("runtime"); + let backend = Arc::new( + WorkerRuntimeExecutionBackend::new( + ProfileRuntimeWorkerFactory::new(root.path()) + .with_runtime_store_dir(&runtime_store) + .with_runtime_id("runtime-subjektiv-test"), + ) + .unwrap(), + ); + let request = backend + .run_on_adapter_runtime(async move { + Ok(saved_subjektiv_restore_fixture(&runtime_store, true).await) + }) + .unwrap(); + assert!(matches!( + backend.restore_worker(request.clone()), + WorkerExecutionSpawnResult::Connected { .. } + )); + let execution = backend.workers.lock().unwrap()[&request.worker_ref].clone(); + let mut transport = Arc::clone(&backend) + .attach_worker_protocol(&request.worker_ref) + .unwrap(); + // A queued frame whose ID has left the bounded buffer must not be + // forwarded with its now-unresolvable pending-text position. + execution + .handle + .send_event(Event::Alert(protocol::Alert { + id: Some("evicted-alert".into()), + position: Some(protocol::AlertPosition::AfterInFlight { block_id: 99 }), + level: protocol::AlertLevel::Warn, + source: protocol::AlertSource::Worker, + message: "stale alert".into(), + timestamp_ms: 1, + })) + .unwrap(); + backend.run_on_adapter_runtime(async move { + tokio::time::timeout(Duration::from_secs(5), async { + while let Some(event) = transport.events.recv().await { + assert!(!matches!(event, Event::Alert(alert) if alert.id.as_deref() == Some("evicted-alert"))); + } + }).await.map_err(|_| "stale Alert did not close its transport".to_string())?; + Ok(()) + }).unwrap(); + execution.handle.alerter.alert( + protocol::AlertLevel::Warn, + protocol::AlertSource::Worker, + "retained alert".into(), + ); + let mut replacement = Arc::clone(&backend) + .attach_worker_protocol(&request.worker_ref) + .unwrap(); + backend.run_on_adapter_runtime(async move { + let event = tokio::time::timeout(Duration::from_secs(5), replacement.events.recv()) + .await.map_err(|_| "replacement Alert replay timed out".to_string())?; + assert!(matches!(event, Some(Event::Alert(alert)) if alert.message == "retained alert" && alert.id.is_some())); + Ok(()) + }).unwrap(); + assert!(backend.stop_worker(&request.worker_ref).is_accepted()); + } + fn profile_stop_fixture( root: &Path, ) -> (WorkerRuntimeExecutionBackend, WorkerExecutionRestoreRequest) { diff --git a/crates/worker/src/discovery.rs b/crates/worker/src/discovery.rs index 68da48ec7..dd7ecb8e8 100644 --- a/crates/worker/src/discovery.rs +++ b/crates/worker/src/discovery.rs @@ -1532,6 +1532,8 @@ mod tests { let mut writer = JsonLineWriter::new(writer_half); writer .write(&Event::Alert(Alert { + id: None, + position: None, level: AlertLevel::Warn, source: AlertSource::Worker, message: "connect-time alert".into(), @@ -1763,6 +1765,8 @@ mod tests { let mut writer = JsonLineWriter::new(stream); writer .write(&Event::Alert(Alert { + id: None, + position: None, level: AlertLevel::Warn, source: AlertSource::Worker, message: "replayed alert".into(), diff --git a/crates/worker/src/in_flight.rs b/crates/worker/src/in_flight.rs index 07568d198..339300cda 100644 --- a/crates/worker/src/in_flight.rs +++ b/crates/worker/src/in_flight.rs @@ -1,11 +1,11 @@ use std::{ collections::HashSet, - sync::{Arc, Mutex, MutexGuard}, + sync::{Arc, Mutex, MutexGuard, Weak}, }; use protocol::{ - CommandEvent, CommandSnapshot, CommandStatus, CommandStream, CommandStreamSlice, Event, - InFlightBlock, InFlightSnapshot, InFlightToolCallState, + AlertPosition, CommandEvent, CommandSnapshot, CommandStatus, CommandStream, CommandStreamSlice, + Event, InFlightBlock, InFlightSnapshot, InFlightToolCallState, }; use session_store::{LoggedContentPart, LoggedItem}; use tokio::sync::broadcast; @@ -38,6 +38,7 @@ enum TrackedBlock { block_id: InFlightBlockId, text: String, finished: bool, + alert_positions: Vec>>, }, Thinking { block_id: InFlightBlockId, @@ -78,6 +79,7 @@ impl InFlightEvents { block_id, text: String::new(), finished: false, + alert_positions: Vec::new(), }); block_id } @@ -213,10 +215,11 @@ impl InFlightEvents { pub(crate) fn clear_for_committed_item_then( &self, item: &LoggedItem, + entry_id: Option<&str>, f: impl FnOnce() -> R, ) -> R { let mut inner = self.lock(); - inner.clear_for_committed_item(item); + inner.clear_for_committed_item(item, entry_id); f() } @@ -378,7 +381,7 @@ impl InFlightInner { } } - fn clear_for_committed_item(&mut self, item: &LoggedItem) { + fn clear_for_committed_item(&mut self, item: &LoggedItem, entry_id: Option<&str>) { match item { LoggedItem::Message { role, content } if matches!(role, session_store::LoggedRole::Assistant) => @@ -391,7 +394,7 @@ impl InFlightInner { }) .collect::(); if !text.is_empty() { - self.remove_first_text_matching(&text); + self.remove_first_text_matching(&text, entry_id); } } LoggedItem::Reasoning { @@ -420,13 +423,55 @@ impl InFlightInner { } } + pub(crate) fn capture_alert_position(&mut self) -> Option>> { + // Prune evicted alerts even when their streamed block has not committed yet. + for block in &mut self.blocks { + if let TrackedBlock::Text { + alert_positions, .. + } = block + { + alert_positions.retain(|position| position.strong_count() > 0); + } + } + for block in self.blocks.iter_mut().rev() { + match block { + TrackedBlock::Text { + block_id, + text, + alert_positions, + .. + } if !text.is_empty() => { + let position = Arc::new(Mutex::new(AlertPosition::AfterInFlight { + block_id: block_id.0, + })); + alert_positions.push(Arc::downgrade(&position)); + return Some(position); + } + TrackedBlock::ToolCall { id, .. } => { + return Some(Arc::new(Mutex::new(AlertPosition::AfterToolCall { + call_id: id.clone(), + }))); + } + // Hidden reasoning and empty text are not conversation rows. + _ => {} + } + } + None + } + fn snapshot(&self) -> InFlightSnapshot { + let (block_ids, blocks) = self + .blocks + .iter() + .filter_map(|block| { + block + .to_snapshot_block() + .map(|snapshot| (block.block_id().0, snapshot)) + }) + .unzip(); InFlightSnapshot { - blocks: self - .blocks - .iter() - .filter_map(TrackedBlock::to_snapshot_block) - .collect(), + block_ids, + blocks, commands: self.commands.clone(), compaction: self.compaction.clone(), } @@ -444,11 +489,25 @@ impl InFlightInner { } } - fn remove_first_text_matching(&mut self, committed: &str) -> bool { + fn remove_first_text_matching(&mut self, committed: &str, entry_id: Option<&str>) -> bool { if let Some(index) = self.blocks.iter().position(|block| match block { TrackedBlock::Text { text, .. } => text == committed, _ => false, }) { + if let ( + TrackedBlock::Text { + alert_positions, .. + }, + Some(entry_id), + ) = (&self.blocks[index], entry_id) + { + for position in alert_positions.iter().filter_map(Weak::upgrade) { + *position.lock().expect("alert position mutex poisoned") = + AlertPosition::AfterEntry { + entry_id: entry_id.to_owned(), + }; + } + } self.blocks.remove(index); true } else { @@ -559,6 +618,72 @@ mod tests { } } + #[test] + fn alert_watchers_are_pruned_and_removed_with_their_text_block() { + let (tx, _) = broadcast::channel(16); + let in_flight = InFlightEvents::new(tx); + let text = in_flight.start_text_block(); + in_flight.text_delta(text, "same".into()); + let first = in_flight.snapshot_guard().capture_alert_position().unwrap(); + drop(first); + let second = in_flight.snapshot_guard().capture_alert_position().unwrap(); + { + let guard = in_flight.snapshot_guard(); + let TrackedBlock::Text { + alert_positions, .. + } = &guard.blocks[0] + else { + panic!("expected text") + }; + assert_eq!(alert_positions.len(), 1); + } + let later = in_flight.start_text_block(); + in_flight.text_delta(later, "same".into()); + let third = in_flight.snapshot_guard().capture_alert_position().unwrap(); + let item = LoggedItem::Message { + role: session_store::LoggedRole::Assistant, + content: vec![LoggedContentPart::Text { + text: "same".into(), + }], + }; + in_flight.clear_for_committed_item_then(&item, Some("first-entry"), || ()); + assert_eq!( + *second.lock().unwrap(), + AlertPosition::AfterEntry { + entry_id: "first-entry".into() + } + ); + assert_eq!( + *third.lock().unwrap(), + AlertPosition::AfterInFlight { block_id: later.0 } + ); + in_flight.clear(); + assert!(in_flight.snapshot_guard().blocks.is_empty()); + assert_eq!( + *third.lock().unwrap(), + AlertPosition::AfterInFlight { block_id: later.0 } + ); + } + + #[test] + fn snapshot_ids_are_parallel_to_emitted_blocks_not_filtered_placeholders() { + let (tx, _) = broadcast::channel(16); + let in_flight = InFlightEvents::new(tx); + in_flight.start_text_block(); + let hidden = in_flight.thinking_start(); + in_flight.thinking_done(hidden, String::new()); + let visible = in_flight.start_text_block(); + in_flight.text_delta(visible, "shown".into()); + let tool = in_flight.tool_call_start("call-1".into(), "Read".into()); + let snapshot = snapshot_from_guard(&in_flight.snapshot_guard()); + assert_eq!(snapshot.blocks.len(), 2); + assert_eq!(snapshot.block_ids, vec![visible.0, tool.0]); + assert_eq!( + snapshot.block_ids, + snapshot_from_guard(&in_flight.snapshot_guard()).block_ids + ); + } + #[test] fn snapshot_boundary_does_not_duplicate_or_gap_delta_sent_after_subscribe() { let (working_event_tx, _) = broadcast::channel(16); @@ -621,7 +746,7 @@ mod tests { // This mirrors Worker::append_entry ordering: clear in-flight first, // then publish the finalized AssistantItem. Identity-aware clients // reconcile this durable commit with the preceding streamed block. - in_flight_for_commit.clear_for_committed_item_then(&assistant_item, || { + in_flight_for_commit.clear_for_committed_item_then(&assistant_item, None, || { sink_for_commit.publish(assistant_entry); }); committed_tx.send(()).unwrap(); @@ -670,7 +795,7 @@ mod tests { entry: crate::session_history::test_logged_history_entry(assistant_item.clone()), }; - in_flight.clear_for_committed_item_then(&assistant_item, || { + in_flight.clear_for_committed_item_then(&assistant_item, None, || { sink.publish(assistant_entry); }); @@ -698,6 +823,7 @@ mod tests { text: "done".into(), }], }, + None, || (), ); @@ -723,6 +849,7 @@ mod tests { encrypted_content: Some("opaque".into()), signature: None, }, + None, || (), ); @@ -746,6 +873,7 @@ mod tests { encrypted_content: Some("opaque".into()), signature: None, }, + None, || (), ); diff --git a/crates/worker/src/ipc/alerter.rs b/crates/worker/src/ipc/alerter.rs index b36ee2b86..b088377d9 100644 --- a/crates/worker/src/ipc/alerter.rs +++ b/crates/worker/src/ipc/alerter.rs @@ -6,7 +6,7 @@ //! //! Each alert is broadcast on the shared `Event` channel and //! also appended to an in-memory buffer so that clients connecting -//! after the fact still see everything emitted during the session. +//! after the fact can replay the bounded recent prefix from the same live Worker. use std::collections::VecDeque; use std::sync::{Arc, Mutex}; @@ -14,7 +14,9 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::broadcast; -use protocol::{Alert, AlertLevel, AlertSource, Event}; +use protocol::{Alert, AlertLevel, AlertPosition, AlertSource, Event}; + +use crate::{in_flight::InFlightEvents, segment_log_sink::SegmentLogSink}; /// Upper bound on buffered alerts. When exceeded, the oldest /// entries are discarded so a long-running session cannot leak @@ -27,9 +29,53 @@ pub struct Alerter { inner: Arc, } +/// A queued identified Alert is no longer retained by this live Worker. +/// Transports must disconnect/resnapshot instead of forwarding its stale anchor. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[error("alert is no longer retained; reconnect for a fresh snapshot")] +pub struct AlertReplayRequired; + struct Inner { working_event_tx: broadcast::Sender, - buffer: Mutex>, + buffer: Mutex>, + position_source: Mutex>, +} + +#[derive(Clone)] +struct PositionSource { + sink: SegmentLogSink, + in_flight: Option, +} + +#[derive(Clone)] +struct BufferedAlert { + alert: Alert, + position: Option>>, +} + +impl BufferedAlert { + fn snapshot(&self) -> Alert { + let mut alert = self.alert.clone(); + alert.position = self.position.as_ref().map(|position| { + position + .lock() + .expect("alert position mutex poisoned") + .clone() + }); + alert + } +} + +impl PositionSource { + fn capture(&self) -> Arc> { + // Same order as commit/snapshot: in-flight -> sink. Never hold the + // alerter buffer lock while acquiring either of these locks. + let mut in_flight = self.in_flight.as_ref().map(InFlightEvents::snapshot_guard); + in_flight + .as_mut() + .and_then(|guard| guard.capture_alert_position()) + .unwrap_or_else(|| Arc::new(Mutex::new(self.sink.alert_position()))) + } } impl Alerter { @@ -38,10 +84,23 @@ impl Alerter { inner: Arc::new(Inner { working_event_tx, buffer: Mutex::new(VecDeque::with_capacity(MAX_BUFFERED_ALERTS)), + position_source: Mutex::new(None), }), } } + pub(crate) fn attach_position_source( + &self, + sink: SegmentLogSink, + in_flight: Option, + ) { + *self + .inner + .position_source + .lock() + .expect("alert source mutex poisoned") = Some(PositionSource { sink, in_flight }); + } + /// Record and broadcast an alert. /// /// The broadcast may have no subscribers (e.g. during Worker @@ -55,35 +114,88 @@ impl Alerter { /// exactly once: older ones from the snapshot, newer ones from /// the freshly-subscribed receiver. pub fn alert(&self, level: AlertLevel, source: AlertSource, message: String) { + self.alert_at(level, source, message, now_ms()); + } + + fn alert_at(&self, level: AlertLevel, source: AlertSource, message: String, timestamp_ms: i64) { + let source_handle = self + .inner + .position_source + .lock() + .expect("alert source mutex poisoned") + .clone(); + let position = source_handle.map(|source| source.capture()); let alert = Alert { + id: Some(uuid::Uuid::now_v7().to_string()), + position: None, level, source, message, - timestamp_ms: now_ms(), + timestamp_ms, }; if let Ok(mut buf) = self.inner.buffer.lock() { if buf.len() >= MAX_BUFFERED_ALERTS { buf.pop_front(); } - buf.push_back(alert.clone()); - let _ = self.inner.working_event_tx.send(Event::Alert(alert)); + let buffered = BufferedAlert { alert, position }; + let event = Event::Alert(buffered.snapshot()); + buf.push_back(buffered); + let _ = self.inner.working_event_tx.send(event); } } + /// Refresh a queued Alert immediately before transport forwarding. + /// + /// The independent log lane may have already delivered the text commit, so + /// the broadcast's copied in-flight anchor is not authoritative anymore. + /// Missing identified alerts require reconnect/resnapshot; legacy alerts + /// without IDs and non-Alert events pass through unchanged. + pub fn freshen_event(&self, event: Event) -> Result { + let Event::Alert(ref alert) = event else { + return Ok(event); + }; + let Some(id) = alert.id.as_ref() else { + return Ok(event); + }; + let buffer = self + .inner + .buffer + .lock() + .expect("alerter buffer mutex poisoned"); + buffer + .iter() + .find(|buffered| buffered.alert.id.as_ref() == Some(id)) + .map(|buffered| Event::Alert(buffered.snapshot())) + .ok_or(AlertReplayRequired) + } + /// Subscribe and atomically snapshot the current buffer. /// /// The returned snapshot contains alerts emitted before /// this call; the receiver will deliver alerts emitted /// after. An alert cannot appear in both. pub fn subscribe_with_snapshot(&self) -> (Vec, broadcast::Receiver) { + let (snapshot, rx) = self.subscribe_with_deferred_snapshot(); + (snapshot(), rx) + } + + // Resolve positions after the session snapshot has been captured. Otherwise + // a text commit between the two snapshots can leave only its old block ID + // in replay, although that block is already absent from the session snapshot. + pub(crate) fn subscribe_with_deferred_snapshot( + &self, + ) -> (impl FnOnce() -> Vec, broadcast::Receiver) { let buf = self .inner .buffer .lock() .expect("alerter buffer mutex poisoned"); let rx = self.inner.working_event_tx.subscribe(); - let snapshot: Vec = buf.iter().cloned().collect(); - (snapshot, rx) + let snapshot: Vec = buf.iter().cloned().collect(); + ( + move || snapshot.iter().map(BufferedAlert::snapshot).collect(), + rx, + ) } } @@ -98,6 +210,266 @@ fn now_ms() -> i64 { mod tests { use super::*; + fn positioned_alerter() -> (Alerter, SegmentLogSink, InFlightEvents) { + let (tx, _) = broadcast::channel(1024); + let in_flight = InFlightEvents::new(tx.clone()); + let sink = SegmentLogSink::new(); + let alerter = Alerter::new(tx); + alerter.attach_position_source(sink.clone(), Some(in_flight.clone())); + (alerter, sink, in_flight) + } + + #[test] + fn queued_alert_uses_committed_anchor_when_log_lane_was_delivered_first() { + let (alerter, sink, in_flight) = positioned_alerter(); + let text = in_flight.start_text_block(); + in_flight.text_delta(text, "finished text".into()); + in_flight.text_done(text, "finished text".into()); + let (_, mut events) = alerter.subscribe_with_snapshot(); + let (_, mut log_entries) = sink.subscribe_with_snapshot(); + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "queued".into()); + let queued = events.try_recv().unwrap(); + let Event::Alert(ref original) = queued else { + panic!("expected alert") + }; + assert!(matches!( + original.position, + Some(AlertPosition::AfterInFlight { .. }) + )); + let original_id = original.id.clone(); + let item: session_store::LoggedItem = agen::Item::assistant_message("finished text").into(); + let entry = crate::session_history::test_logged_history_entry(item.clone()); + let entry_id = entry.metadata.entry_id.0.clone(); + in_flight.clear_for_committed_item_then(&item, Some(&entry_id), || { + sink.publish(session_store::LogEntry::AnnotatedAssistantItem { ts: 42, entry }); + }); + let committed = + crate::ipc::protocol_session::live_log_entry_event(log_entries.try_recv().unwrap()) + .unwrap(); + assert!( + matches!(committed, Event::SessionEntryCommitted { entry } if entry.entry_id == entry_id) + ); + let Event::Alert(forwarded) = alerter.freshen_event(queued).unwrap() else { + panic!("expected alert") + }; + assert_eq!(forwarded.id, original_id); + assert_eq!( + forwarded.position, + Some(AlertPosition::AfterEntry { entry_id }) + ); + let replay = alerter.subscribe_with_snapshot().0; + assert_eq!( + serde_json::to_value(forwarded).unwrap(), + serde_json::to_value(&replay[0]).unwrap() + ); + } + + #[test] + fn evicted_or_unknown_alert_ids_require_replay_but_legacy_events_pass_through() { + let (alerter, _, _) = positioned_alerter(); + let (_, mut events) = alerter.subscribe_with_snapshot(); + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "old".into()); + let queued = events.try_recv().unwrap(); + for _ in 0..MAX_BUFFERED_ALERTS { + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "later".into()); + } + assert!(matches!( + alerter.freshen_event(queued), + Err(AlertReplayRequired) + )); + let unknown = Event::Alert(Alert { + id: Some("not-retained".into()), + position: Some(AlertPosition::AfterInFlight { block_id: 1 }), + level: AlertLevel::Warn, + source: AlertSource::Worker, + message: "unknown".into(), + timestamp_ms: 42, + }); + assert!(matches!( + alerter.freshen_event(unknown), + Err(AlertReplayRequired) + )); + let legacy = Event::Alert(Alert { + id: None, + position: None, + level: AlertLevel::Warn, + source: AlertSource::Worker, + message: "legacy".into(), + timestamp_ms: 42, + }); + let expected = serde_json::to_value(&legacy).unwrap(); + assert_eq!( + serde_json::to_value(alerter.freshen_event(legacy).unwrap()).unwrap(), + expected + ); + assert!(matches!( + alerter.freshen_event(Event::Shutdown), + Ok(Event::Shutdown) + )); + } + + #[test] + fn rowless_run_transitions_preserve_visible_alert_anchor_live_and_on_replay() { + use session_store::LogEntry; + let (alerter, sink, _) = positioned_alerter(); + let entry = crate::session_history::test_logged_history_entry( + agen::Item::assistant_message("visible"), + ); + let expected = Some(AlertPosition::AfterEntry { + entry_id: entry.metadata.entry_id.0.clone(), + }); + sink.publish(LogEntry::AnnotatedAssistantItem { ts: 1, entry }); + let (_, mut events) = alerter.subscribe_with_snapshot(); + let transitions = [ + LogEntry::RunCancelled { + ts: 2, + entry_id: None, + }, + LogEntry::RunYielded { + ts: 3, + entry_id: None, + reason: protocol::RunYieldReason::Compaction, + active_run_turn_count: 1, + }, + LogEntry::RunResumed { + ts: 4, + entry_id: None, + source: protocol::RunResumeSource::Compaction, + active_run_turn_count: 1, + }, + ]; + for mut transition in transitions { + transition.ensure_session_entry_id(); + sink.publish(transition); + alerter.alert( + AlertLevel::Warn, + AlertSource::Worker, + "after transition".into(), + ); + let Event::Alert(live) = alerter.freshen_event(events.try_recv().unwrap()).unwrap() + else { + panic!("expected alert") + }; + assert_eq!(live.position, expected); + } + let replay = alerter.subscribe_with_snapshot().0; + assert_eq!(replay.len(), 3); + assert!(replay.iter().all(|alert| alert.position == expected)); + let (entries, _) = sink.subscribe_with_snapshot(); + // Rebuilding the sink cache from retained entries must use the same rule. + sink.replace_silent(entries); + alerter.alert( + AlertLevel::Warn, + AlertSource::Worker, + "after cache rebuild".into(), + ); + assert_eq!( + alerter.subscribe_with_snapshot().0.last().unwrap().position, + expected + ); + } + + #[test] + fn identical_alerts_in_same_millisecond_keep_distinct_ids_across_replay() { + let (alerter, _, _) = positioned_alerter(); + let (_, mut rx) = alerter.subscribe_with_snapshot(); + for _ in 0..2 { + alerter.alert_at(AlertLevel::Warn, AlertSource::Worker, "same".into(), 42); + } + let (snapshot, _) = alerter.subscribe_with_snapshot(); + assert_ne!(snapshot[0].id, snapshot[1].id); + for expected in &snapshot { + let Event::Alert(live) = rx.try_recv().unwrap() else { + panic!("expected alert") + }; + assert!(live.id.is_some()); + assert_eq!(live.id, expected.id); + assert_eq!(live.timestamp_ms, 42); + assert_eq!(live.position, Some(AlertPosition::Start)); + assert_eq!(live.position, expected.position); + } + let (replay, _) = alerter.subscribe_with_snapshot(); + assert_eq!( + serde_json::to_value(snapshot).unwrap(), + serde_json::to_value(replay).unwrap() + ); + } + + #[test] + fn tool_call_anchor_survives_commit_and_new_text() { + let (alerter, sink, in_flight) = positioned_alerter(); + in_flight.tool_call_start("call-1".into(), "Read".into()); + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "tool alert".into()); + let item: session_store::LoggedItem = agen::Item::tool_call("call-1", "Read", "{}").into(); + let entry = session_store::LogEntry::AnnotatedAssistantItem { + ts: 1, + entry: crate::session_history::test_logged_history_entry(item.clone()), + }; + in_flight.clear_for_committed_item_then(&item, Some("entry-tool"), || sink.publish(entry)); + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "after commit".into()); + let text = in_flight.start_text_block(); + in_flight.text_delta(text, "next".into()); + let (snapshot, _) = alerter.subscribe_with_snapshot(); + for alert in snapshot { + assert_eq!( + alert.position, + Some(AlertPosition::AfterToolCall { + call_id: "call-1".into() + }) + ); + } + } + + #[test] + fn eviction_drops_position_ownership_and_clear_does_not_retarget_old_alerts() { + let (alerter, _, in_flight) = positioned_alerter(); + let text = in_flight.start_text_block(); + in_flight.text_delta(text, "discarded".into()); + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "old".into()); + let weak = { + let buffer = alerter.inner.buffer.lock().unwrap(); + Arc::downgrade(buffer[0].position.as_ref().unwrap()) + }; + for _ in 0..MAX_BUFFERED_ALERTS { + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "new".into()); + } + assert!(weak.upgrade().is_none()); + let (before, _) = alerter.subscribe_with_snapshot(); + in_flight.clear(); + let next = in_flight.start_text_block(); + in_flight.text_delta(next, "discarded".into()); + let item = session_store::LoggedItem::Message { + role: session_store::LoggedRole::Assistant, + content: vec![session_store::LoggedContentPart::Text { + text: "discarded".into(), + }], + }; + in_flight.clear_for_committed_item_then(&item, Some("unrelated-entry"), || ()); + let (after, _) = alerter.subscribe_with_snapshot(); + assert_eq!(before.len(), MAX_BUFFERED_ALERTS); + assert_eq!( + serde_json::to_value(before).unwrap(), + serde_json::to_value(after).unwrap() + ); + } + + #[test] + fn snapshot_does_not_acquire_in_flight_lock_under_buffer_lock() { + let (alerter, _, in_flight) = positioned_alerter(); + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "before".into()); + let guard = in_flight.snapshot_guard(); + let cloned = alerter.clone(); + let (tx, rx) = std::sync::mpsc::channel(); + let thread = std::thread::spawn(move || { + let (snapshot, _) = cloned.subscribe_with_snapshot(); + tx.send(snapshot).unwrap(); + }); + let result = rx.recv_timeout(std::time::Duration::from_secs(5)); + drop(guard); + thread.join().unwrap(); + assert_eq!(result.unwrap().len(), 1); + } + #[test] fn alert_broadcasts_to_existing_subscriber() { let (tx, _keep) = broadcast::channel::(8); diff --git a/crates/worker/src/ipc/protocol_session.rs b/crates/worker/src/ipc/protocol_session.rs index 2ca3e2e7f..6735afdcf 100644 --- a/crates/worker/src/ipc/protocol_session.rs +++ b/crates/worker/src/ipc/protocol_session.rs @@ -30,12 +30,12 @@ fn subscribe_before_snapshot( pub fn subscribe_worker_protocol_session(handle: &WorkerHandle) -> WorkerProtocolSessionStreams { let ((snapshot_event, log_entries), (alert_snapshot, events)) = subscribe_before_snapshot( - || handle.alerter.subscribe_with_snapshot(), + || handle.alerter.subscribe_with_deferred_snapshot(), || handle.snapshot_event_with_entry_subscription(), ); WorkerProtocolSessionStreams { snapshot_event, - alert_snapshot, + alert_snapshot: alert_snapshot(), log_entries, events, } diff --git a/crates/worker/src/ipc/server.rs b/crates/worker/src/ipc/server.rs index 8df81da4b..03f0c52a8 100644 --- a/crates/worker/src/ipc/server.rs +++ b/crates/worker/src/ipc/server.rs @@ -154,6 +154,11 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: WorkerHandle) event = streams.events.recv() => { match event { Ok(event) => { + let Ok(event) = handle.alerter.freshen_event(event) else { + // The queued Alert was evicted; reconnect must rebuild + // the bounded prefix rather than forward a stale anchor. + break; + }; if writer.write_event(&event).await.is_err() { break; } diff --git a/crates/worker/src/segment_log_sink.rs b/crates/worker/src/segment_log_sink.rs index a21d9cec5..d498acb1e 100644 --- a/crates/worker/src/segment_log_sink.rs +++ b/crates/worker/src/segment_log_sink.rs @@ -24,6 +24,7 @@ use std::sync::{Arc, Mutex as StdMutex}; +use protocol::{AlertPosition, SessionSnapshotEntryData}; use session_store::LogEntry; use tokio::sync::broadcast; @@ -46,7 +47,7 @@ pub struct SegmentLogSink { struct SinkInner { /// Full session log mirror in commit order. Reset on session swap /// (compaction / fork) via [`SegmentLogSink::reset_with_initial`]. - mirror: StdMutex>, + mirror: StdMutex, /// Broadcast channel for live entry updates. The same `Sender` /// survives session swaps so existing subscribers keep their /// receiver — they observe the swap as a freshly broadcast @@ -54,6 +55,46 @@ struct SinkInner { session_entry_tx: broadcast::Sender, } +struct Mirror { + entries: Vec, + alert_position: AlertPosition, +} + +impl Mirror { + fn new(entries: Vec) -> Self { + let alert_position = latest_alert_position(&entries).unwrap_or(AlertPosition::Start); + Self { + entries, + alert_position, + } + } +} + +// Use the public projection so private reasoning/response boundaries cannot become +// anchors. Results/accounting update existing rows, and non-error Run lifecycle +// transitions have no conversation row of their own. +fn latest_alert_position(entries: &[LogEntry]) -> Option { + session_store::public_snapshot::project_current_session_snapshot(entries) + .entries + .into_iter() + .rev() + .find_map(|entry| match entry.data { + SessionSnapshotEntryData::ToolCall { call_id, .. } => { + Some(AlertPosition::AfterToolCall { call_id }) + } + SessionSnapshotEntryData::ToolResult { .. } + | SessionSnapshotEntryData::Invoke { .. } + | SessionSnapshotEntryData::Usage { .. } + | SessionSnapshotEntryData::RunStats { .. } + | SessionSnapshotEntryData::RunYielded { .. } + | SessionSnapshotEntryData::RunResumed { .. } + | SessionSnapshotEntryData::RunCancelled => None, + _ => Some(AlertPosition::AfterEntry { + entry_id: entry.entry_id, + }), + }) +} + impl SegmentLogSink { /// Create a fresh sink with an empty mirror. Used before any entry /// has been written (deferred SegmentStart) or as a placeholder in @@ -62,7 +103,7 @@ impl SegmentLogSink { let (session_entry_tx, _) = broadcast::channel(BROADCAST_CAPACITY); Self { inner: Arc::new(SinkInner { - mirror: StdMutex::new(Vec::new()), + mirror: StdMutex::new(Mirror::new(Vec::new())), session_entry_tx, }), } @@ -75,7 +116,7 @@ impl SegmentLogSink { let (session_entry_tx, _) = broadcast::channel(BROADCAST_CAPACITY); Self { inner: Arc::new(SinkInner { - mirror: StdMutex::new(entries), + mirror: StdMutex::new(Mirror::new(entries)), session_entry_tx, }), } @@ -103,7 +144,10 @@ impl SegmentLogSink { .mirror .lock() .expect("session log mirror mutex poisoned"); - mirror.push(entry.clone()); + if let Some(position) = latest_alert_position(std::slice::from_ref(&entry)) { + mirror.alert_position = position; + } + mirror.entries.push(entry.clone()); if Self::is_live_relevant(&entry) { // SendError means there are zero subscribers; harmless. The // mirror lock is held across `send` so subscribers cannot @@ -147,8 +191,7 @@ impl SegmentLogSink { .mirror .lock() .expect("session log mirror mutex poisoned"); - mirror.clear(); - mirror.push(initial.clone()); + *mirror = Mirror::new(vec![initial.clone()]); let _ = self.inner.session_entry_tx.send(initial); } @@ -164,7 +207,7 @@ impl SegmentLogSink { .mirror .lock() .expect("session log mirror mutex poisoned"); - *mirror = entries; + *mirror = Mirror::new(entries); if let Some(initial) = first { let _ = self.inner.session_entry_tx.send(initial); } @@ -181,7 +224,7 @@ impl SegmentLogSink { .mirror .lock() .expect("session log mirror mutex poisoned"); - *mirror = entries; + *mirror = Mirror::new(entries); } /// Truncate the mirror without broadcasting. @@ -191,7 +234,9 @@ impl SegmentLogSink { .mirror .lock() .expect("session log mirror mutex poisoned"); - mirror.truncate(entries_len); + mirror.entries.truncate(entries_len); + mirror.alert_position = + latest_alert_position(&mirror.entries).unwrap_or(AlertPosition::Start); } /// Atomically read the current mirror and subscribe to subsequent @@ -203,17 +248,27 @@ impl SegmentLogSink { .mirror .lock() .expect("session log mirror mutex poisoned"); - let snapshot = mirror.clone(); + let snapshot = mirror.entries.clone(); let rx = self.inner.session_entry_tx.subscribe(); (snapshot, rx) } + pub(crate) fn alert_position(&self) -> AlertPosition { + self.inner + .mirror + .lock() + .expect("session log mirror mutex poisoned") + .alert_position + .clone() + } + /// Current entry count. Useful for tests / diagnostics. pub fn len(&self) -> usize { self.inner .mirror .lock() .expect("session log mirror mutex poisoned") + .entries .len() } @@ -267,6 +322,70 @@ mod tests { } } + #[test] + fn alert_anchor_tracks_visible_rows_not_reasoning_results_or_accounting() { + let sink = SegmentLogSink::new(); + assert_eq!(sink.alert_position(), AlertPosition::Start); + sink.publish(session_start()); + assert_eq!(sink.alert_position(), AlertPosition::Start); + let user = user_input("hello"); + sink.publish(user.clone()); + let expected = latest_alert_position(&[user.clone()]).unwrap(); + assert_eq!(sink.alert_position(), expected); + sink.publish(turn_end(1)); + sink.publish(LogEntry::Invoke { + ts: 2, + trigger: protocol::InvokeKind::UserSend, + }); + sink.publish(LogEntry::AnnotatedAssistantItem { + ts: 3, + entry: crate::session_history::test_logged_history_entry( + session_store::LoggedItem::Reasoning { + text: "private".into(), + summary: vec![], + encrypted_content: None, + signature: None, + }, + ), + }); + assert_eq!(sink.alert_position(), expected); + let tool = LogEntry::AnnotatedAssistantItem { + ts: 4, + entry: crate::session_history::test_logged_history_entry(agen::Item::tool_call( + "call-1", "Read", "{}", + )), + }; + sink.publish(tool.clone()); + assert_eq!( + sink.alert_position(), + AlertPosition::AfterToolCall { + call_id: "call-1".into() + } + ); + sink.publish(user_input("newer row")); + let newer = sink.alert_position(); + sink.publish(LogEntry::AnnotatedToolResult { + ts: 5, + entry: crate::session_history::test_logged_history_entry(agen::Item::tool_result( + "call-1", "done", + )), + }); + assert_eq!(sink.alert_position(), newer); + sink.replace_silent(vec![session_start(), user.clone(), tool]); + assert_eq!( + sink.alert_position(), + AlertPosition::AfterToolCall { + call_id: "call-1".into() + } + ); + sink.truncate_silent(2); + assert_eq!(sink.alert_position(), expected); + sink.reset_with_initial(session_start()); + assert_eq!(sink.alert_position(), AlertPosition::Start); + sink.reset_with_initial_entries(vec![session_start(), user]); + assert_eq!(sink.alert_position(), expected); + } + #[test] fn publish_then_subscribe_returns_history_in_snapshot() { let sink = SegmentLogSink::new(); diff --git a/crates/worker/src/spawn/comm_tools.rs b/crates/worker/src/spawn/comm_tools.rs index 14f4ab3fe..9fca770de 100644 --- a/crates/worker/src/spawn/comm_tools.rs +++ b/crates/worker/src/spawn/comm_tools.rs @@ -131,6 +131,8 @@ mod tests { listener, vec![ Event::Alert(Alert { + id: None, + position: None, level: AlertLevel::Warn, source: AlertSource::Worker, message: "replayed alert".into(), diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 9f3349402..74979bed4 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -1918,11 +1918,13 @@ where self.state.increment_entries(); if let Some(in_flight) = &self.in_flight { let committed_item = match &entry { - LogEntry::AnnotatedAssistantItem { entry, .. } => Some(entry.item.clone()), + LogEntry::AnnotatedAssistantItem { entry, .. } => { + Some((entry.item.clone(), entry.metadata.entry_id.0.clone())) + } _ => None, }; - if let Some(item_for_clear) = committed_item { - in_flight.clear_for_committed_item_then(&item_for_clear, || { + if let Some((item_for_clear, entry_id)) = committed_item { + in_flight.clear_for_committed_item_then(&item_for_clear, Some(&entry_id), || { self.sink.publish(entry); }); return Ok(()); @@ -4253,6 +4255,7 @@ impl Worker { /// Worker-internal operations (compaction failures, AGENTS.md /// ingestion warnings) can surface messages to connected clients. pub fn attach_alerter(&mut self, alerter: Alerter) { + alerter.attach_position_source(self.sink.clone(), self.in_flight.clone()); self.alerter = Some(alerter); } @@ -13313,6 +13316,77 @@ mod build_summary_prompt_tests { } } + // Real Worker wiring, filesystem Store, log writer and reconnect buffers; + // only the model client is inert because this test never invokes the engine. + #[tokio::test] + async fn alert_pending_text_anchor_resolves_at_durable_commit_before_replay() { + let dir = tempfile::tempdir().unwrap(); + let cwd = dir.path().join("workspace"); + std::fs::create_dir_all(&cwd).unwrap(); + let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap(); + let mut worker = Worker::new( + minimal_manifest(), + Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient), + store, + WorkerWorkspaceContext::no_workspace(), + WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone()), + Scope::writable(&cwd).unwrap(), + ) + .await + .unwrap(); + let (tx, _) = broadcast::channel(16); + let in_flight = InFlightEvents::new(tx.clone()); + worker.in_flight = Some(in_flight.clone()); + let alerter = Alerter::new(tx); + worker.attach_alerter(alerter.clone()); + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "before head".into()); + assert_eq!( + alerter.subscribe_with_snapshot().0[0].position, + Some(protocol::AlertPosition::Start) + ); + worker.materialize_durable_session_head().await.unwrap(); + let text = in_flight.start_text_block(); + in_flight.text_delta(text, "streamed".into()); + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "during text".into()); + let (before, _) = alerter.subscribe_with_snapshot(); + let snapshot = crate::in_flight::snapshot_from_guard(&in_flight.snapshot_guard()); + assert_eq!( + before[1].position, + Some(protocol::AlertPosition::AfterInFlight { + block_id: snapshot.block_ids[0] + }) + ); + let (deferred, _) = alerter.subscribe_with_deferred_snapshot(); + let entry = crate::session_history::test_logged_history_entry( + agen::Item::assistant_message("streamed"), + ); + let entry_id = entry.metadata.entry_id.0.clone(); + worker + .log_writer_handle() + .append_entry(LogEntry::AnnotatedAssistantItem { ts: 42, entry }) + .unwrap(); + let (entries, _) = worker.sink.subscribe_with_snapshot(); + assert!(entries.iter().any(|entry| matches!(entry, LogEntry::AnnotatedAssistantItem { entry, .. } if entry.metadata.entry_id.0 == entry_id))); + assert!( + crate::in_flight::snapshot_from_guard(&in_flight.snapshot_guard()) + .blocks + .is_empty() + ); + let replay = deferred(); + assert_eq!(replay[1].id, before[1].id); + assert_eq!( + replay[1].position, + Some(protocol::AlertPosition::AfterEntry { + entry_id: entry_id.clone() + }) + ); + alerter.alert(AlertLevel::Warn, AlertSource::Worker, "after commit".into()); + assert_eq!( + alerter.subscribe_with_snapshot().0[2].position, + Some(protocol::AlertPosition::AfterEntry { entry_id }) + ); + } + #[tokio::test] async fn materialize_durable_session_head_commits_without_model_run() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index ae4f30561..1c8504c6c 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -46269,6 +46269,7 @@ mod tests { }, state: self.state.lock().unwrap().clone(), in_flight: protocol::InFlightSnapshot { + block_ids: Vec::new(), blocks: Vec::new(), commands: Vec::new(), compaction: None, diff --git a/web/workspace/deno.json b/web/workspace/deno.json index a1d4b1e2f..f87d91081 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -6,7 +6,7 @@ "dev": "deno run -A npm:vite@8.3.2 dev", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787", "check": "deno run -A npm:@sveltejs/kit@3.0.0 sync && deno run -A npm:svelte-check@4.7.6 --tsconfig ./tsconfig.json", - "test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/navigation/deferred.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/api/ticket-case.test.ts src/lib/workspace/api/ticket-operations.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/settings/workdir-checkout.test.ts tests/workdir-removal.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/workers/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts src/lib/workspace/tickets/ticket-search.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts src/lib/workspace/drive/api.test.ts src/lib/workspace/drive/controller.test.ts src/lib/workspace/drive-ui/policy.test.ts", + "test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/navigation/deferred.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/api/ticket-case.test.ts src/lib/workspace/api/ticket-operations.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/alert-order.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/settings/workdir-checkout.test.ts tests/workdir-removal.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/workers/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts src/lib/workspace/tickets/ticket-search.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts src/lib/workspace/drive/api.test.ts src/lib/workspace/drive/controller.test.ts src/lib/workspace/drive-ui/policy.test.ts", "test:component": "deno run -A npm:vitest@5.0.3 run --config vitest.config.ts", "test:browser:empty-workspace": "deno test --config ../../tools/web-ux/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/visual-review/empty-workspace.browser.test.ts", "test:browser:drive": "deno test --config test/drive-browser/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/drive-browser/drive.browser.test.ts", diff --git a/web/workspace/src/lib/generated/protocol-validator.ts b/web/workspace/src/lib/generated/protocol-validator.ts index ed09d4b86..b75fa7738 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"]},"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","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"},"CompletionContext":{"additionalProperties":false,"properties":{"argument":{"type":["string","null"]},"invocation":{"$ref":"#/$defs/FeatureInvocationIdentity"}},"required":["invocation"],"type":"object"},"CompletionEntry":{"additionalProperties":false,"description":"One completion candidate for a prefix query.","properties":{"description":{"type":["string","null"]},"invocation":{"anyOf":[{"$ref":"#/$defs/FeatureInvocationDescriptor"},{"type":"null"}]},"is_dir":{"default":false,"type":"boolean"},"usage":{"type":["string","null"]},"value":{"type":"string"}},"required":["value","is_dir"],"type":"object"},"CompletionKind":{"description":"Kind of completion requested by `Method::ListCompletions`.\n\n`File` is the existing `@` path lane. `Feature` lists installed chat\ninvocations. `FeatureArgument` resolves a declared argument using the\nsource-qualified [`CompletionContext`].","enum":["file","feature","feature_argument"],"type":"string"},"ContextTokenSource":{"description":"Provenance of the current-context token count in a [`Greeting`].","oneOf":[{"const":"measured","description":"Provider-reported input occupancy from the most recent LLM request.","type":"string"},{"const":"estimated","description":"Current history projected from the Worker's token accounting records.","type":"string"}]},"ContextUsage":{"additionalProperties":false,"description":"Current prompt-context occupancy, distinct from cumulative run traffic.","properties":{"source":{"$ref":"#/$defs/ContextTokenSource"},"tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["tokens","source"],"type":"object"},"ErrorCode":{"enum":["already_running","not_running","not_paused","provider_error","tool_error","invalid_request","internal"],"type":"string"},"Event":{"oneOf":[{"additionalProperties":false,"description":"Durable Submit acceptance. A `Queued` receipt follows the durable FIFO\ncheckpoint. A `Started` receipt follows the pending activation checkpoint;\nthe corresponding [`Event::UserMessage`] marks the later history commit.\nRepeating the same request id and exact payload returns the same receipt\nwithout appending or activating twice.","properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"$ref":"#/$defs/SubmissionDisposition"},"submission_id":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","submission_id","disposition"],"type":"object"},"event":{"const":"submission_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","message"],"type":"object"},"event":{"const":"submission_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Durable notification acceptance. The request identity is persisted with\nthe notification receipt before this event is emitted. Repeating the\nsame request id and exact payload returns the same acknowledgement\nwithout appending the notification twice.","properties":{"data":{"additionalProperties":false,"properties":{"notification_request_id":{"type":"string"}},"required":["notification_request_id"],"type":"object"},"event":{"const":"notification_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated notification rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"},"event":{"const":"notification_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Complete FIFO replacement following enqueue, activation, cancel, or clear.\nClients replace their pending view in stream order; reconnect uses the snapshot.","properties":{"data":{"additionalProperties":false,"properties":{"pending":{"$ref":"#/$defs/PendingSubmissionsSnapshot"}},"required":["pending"],"type":"object"},"event":{"const":"pending_submissions_changed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A user input message persisted as `LogEntry::AnnotatedUserInput` and\nactivated for a turn. Broadcast to every subscribed client so TUI / GUI\ninstances show the same user line that reconnect snapshots replay.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Older mixed-version producers omit it.","type":["string","null"]},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"}},"required":["segments"],"type":"object"},"event":{"const":"user_message","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A canonical assistant/tool entry has committed to durable history.\nStreaming events remain the in-flight authority; this event supplies the\nstable identity used to reconcile that live block with paged history.","properties":{"data":{"additionalProperties":false,"properties":{"entry":{"$ref":"#/$defs/SessionSnapshotEntry"}},"required":["entry"],"type":"object"},"event":{"const":"session_entry_committed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One agent-injected system item committed to history.\n\nCarries the JSON form of `session_store::SystemItem`. Covers\n`Method::Notify` echoes, child-Worker lifecycle events from\n`Method::WorkerEvent`, `@` resolution payloads, and any future\nClients dispatch on the `kind` tag for typed rendering instead\nof parsing free-text prefixes like `[Notification] …` or\n`[File: …]`.\n\nOne event per `LogEntry::AnnotatedSystemItem` commit. Disk-side and\nwire-side are 1:1.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Synthetic compatibility events omit it.","type":["string","null"]},"item":true},"required":["item"],"type":"object"},"event":{"const":"system_item","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A new self-driving cycle has begun (IDLE → active transition).\n\nMarker event for the start of an Invoke range; the range extends\nimplicitly until the next `InvokeStart`. Fires for every accepted\n`Method::Submit` (kind=`UserSend`), `Method::Notify` (kind=`Notify`),\n`Method::WorkerEvent` re-injection (kind=`WorkerEvent`), and any other\nIDLE-breaking trigger. Mid-run interrupts (e.g. hook output,\ntyped system reminder insertion that doesn't break IDLE) do not\nemit `InvokeStart` — they appear as `SystemItem` only.\n\nCarries `kind` only; the payload (user text / notify message /\nworker event body) is delivered separately via the immediately\nfollowing `UserMessage` / `SystemItem` event.","properties":{"data":{"additionalProperties":false,"properties":{"kind":{"$ref":"#/$defs/InvokeKind"},"timestamp_ms":{"description":"Timestamp of the persisted Invoke, not the client's attach time.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"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"]},"timestamp_ms":{"description":"Present for usage delivered from the committed session-log lane.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"type":"object"},"event":{"const":"usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Authoritative current-context occupancy after a non-request boundary.\n\nUnlike [`Event::Usage`], this is not cumulative request traffic. It lets\nconnected clients converge with reconnect snapshots after run completion,\ncompaction, rewind, and other history rewrites. `None` explicitly means\nthe current value is unavailable and stale occupancy must be cleared.","properties":{"data":{"additionalProperties":false,"properties":{"usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}]}},"type":"object"},"event":{"const":"context_usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/RunResult"}},"required":["result"],"type":"object"},"event":{"const":"run_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/ErrorCode"},"message":{"type":"string"}},"required":["code","message"],"type":"object"},"event":{"const":"error","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Sent exactly once at the start of every client connection.\n\n`entries` is the session-log mirror at subscribe time, serialised\nas the JSON form of `session_store::LogEntry`. This is the\nbulk-reconstruction lane: clients walk the entries to seed their\nderived view.\n\n`greeting` and `status` accompany the snapshot so clients render\nworker identity and current controller state without an extra round\ntrip.\n\nLive updates after the snapshot arrive through the streaming\nevents (`TextDelta` / `ToolCall*` / `ToolResult` / etc.) plus\nrole-specific entry events (`SegmentRotated` / `SystemItem`) —\nthere is no generic \"every committed entry\" broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"greeting":{"$ref":"#/$defs/Greeting"},"in_flight":{"$ref":"#/$defs/InFlightSnapshot","description":"Unfinished model output that has already streamed in the current\nrun but is not yet represented by committed snapshot entries."},"internal_workers":{"description":"Parent-owned Internal Worker sessions visible to this client.\nService-private Internal Workers are deliberately excluded.","items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"Full 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. Apply in arrival order on the parent's serialized stream; a\nreconnect snapshot replaces the projection before live events resume.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","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":{"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker"],"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":{"context":{"anyOf":[{"$ref":"#/$defs/CompletionContext"},{"type":"null"}]},"entries":{"items":{"$ref":"#/$defs/CompletionEntry"},"type":"array"},"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"default":"","type":"string"},"request_id":{"type":["string","null"]}},"required":["kind","entries","prefix"],"type":"object"},"event":{"const":"completions","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"}]},"FeatureInvocation":{"additionalProperties":false,"properties":{"arguments":{"items":{"$ref":"#/$defs/InvocationArgumentValue"},"type":"array"},"identity":{"$ref":"#/$defs/FeatureInvocationIdentity"},"invocation_id":{"description":"Stable client-generated id retained across queueing and transport retry.","type":"string"},"name":{"description":"Selected public name, retained for exact draft/history restoration. Host\nresolution is always by `identity`.","type":"string"}},"required":["invocation_id","identity","name","arguments"],"type":"object"},"FeatureInvocationDescriptor":{"additionalProperties":false,"properties":{"aliases":{"default":[],"items":{"type":"string"},"type":"array"},"arguments":{"default":[],"items":{"$ref":"#/$defs/InvocationArgumentDescriptor"},"type":"array"},"client_adapter":{"anyOf":[{"$ref":"#/$defs/InvocationClientAdapter"},{"type":"null"}]},"description":{"type":"string"},"display_name":{"type":"string"},"identity":{"$ref":"#/$defs/FeatureInvocationIdentity"},"name":{"type":"string"},"syntax":{"$ref":"#/$defs/FeatureInvocationSyntax"}},"required":["identity","name","display_name","description","syntax","aliases","arguments"],"type":"object"},"FeatureInvocationIdentity":{"description":"Source-qualified identity of one invocation contribution, for example\n`builtin:attachments/attach`. Display names and aliases are never authority.","type":"string"},"FeatureInvocationSyntax":{"oneOf":[{"const":"parenthesized","description":"`/name(value, key=value)` with JSON-style quoted strings and `)` as the\nexplicit boundary before following natural language.","type":"string"}]},"Greeting":{"additionalProperties":false,"description":"Worker self-description rendered by clients when a session starts empty.\n\nSnapshot metadata contains only resolved public execution information so\nclients do not need their own copy of the manifest.","properties":{"context_tokens":{"default":0,"description":"Legacy numeric mirror retained for mixed-version clients.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"context_usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}],"description":"Typed current-context value. Older snapshots omit it; clients may fall\nback to `context_tokens` only when `context_window` is known."},"context_window":{"default":0,"description":"Resolved effective model context window in tokens. Legacy producers use\nzero when this is unavailable; clients must treat that as unknown.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"cwd":{"type":"string"},"model":{"type":"string"},"provider":{"type":"string"},"reasoning":{"anyOf":[{"$ref":"#/$defs/ReasoningConfig"},{"type":"null"}],"description":"Effective reasoning control. Absent means unspecified or unavailable."},"scope_summary":{"type":"string"},"tools":{"items":{"type":"string"},"type":"array"},"worker_name":{"type":"string"}},"required":["worker_name","cwd","provider","model","scope_summary","tools","context_tokens","context_window"],"type":"object"},"InFlightBlock":{"oneOf":[{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"thinking","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"type":"string"},"id":{"type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"state":{"$ref":"#/$defs/InFlightToolCallState"}},"required":["kind","id","name","args"],"type":"object"}]},"InFlightCompaction":{"additionalProperties":false,"properties":{"phase":{"$ref":"#/$defs/CompactionPhase"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"trigger":{"$ref":"#/$defs/CompactionTrigger"}},"required":["phase","started_at_ms","trigger"],"type":"object"},"InFlightSnapshot":{"additionalProperties":false,"description":"Unfinished model output and active command state included in\n`Event::Snapshot` for clients that attach while work is still streaming.\n\nThese blocks are presentation state only: they are reconstructed from the\nactive Worker controller and must not be treated as committed assistant\nhistory. Finalized assistant items continue to come from ordinary snapshot\nentries.","properties":{"blocks":{"items":{"$ref":"#/$defs/InFlightBlock"},"type":"array"},"commands":{"items":{"$ref":"#/$defs/CommandSnapshot"},"type":"array"},"compaction":{"anyOf":[{"$ref":"#/$defs/InFlightCompaction"},{"type":"null"}],"description":"The currently running compaction, if any.\n\nThis is lifecycle progress only. Candidate history and the staged\nSegment remain private until the Segment is activated atomically."}},"type":"object"},"InFlightToolCallState":{"enum":["pending","streaming_args","done"],"type":"string"},"InternalWorkerKind":{"description":"Presentation category for an Internal Worker exposed through its parent's\nprotocol stream. Internal Workers never become independently addressable\nprotocol subjects.","oneOf":[{"enum":["sub_worker"],"type":"string"},{"additionalProperties":false,"properties":{"service":{"additionalProperties":false,"properties":{"kind":{"type":"string"}},"required":["kind"],"type":"object"}},"required":["service"],"type":"object"}]},"InternalWorkerRef":{"additionalProperties":false,"description":"Stable presentation identity for one parent-owned Internal Worker session.\n`name` is display-only; `session_id` is the identity used by clients.","properties":{"kind":{"$ref":"#/$defs/InternalWorkerKind"},"name":{"type":"string"},"parent_session_id":{"type":["string","null"]},"session_id":{"type":"string"}},"required":["session_id","name","kind"],"type":"object"},"InternalWorkerSnapshot":{"additionalProperties":false,"description":"Reconnect state for one visible Internal Worker. The parent serializes\nsnapshot publication and subsequent live events on the same ordered stream.","properties":{"error":{"type":["string","null"]},"greeting":{"anyOf":[{"$ref":"#/$defs/Greeting"},{"type":"null"}],"description":"Public execution metadata for this child. Older snapshots and service\nworkers may omit it; clients must never substitute the parent's values."},"in_flight":{"$ref":"#/$defs/InFlightSnapshot"},"internal_workers":{"items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"status":{"$ref":"#/$defs/WorkerStatus","default":"idle"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","session","status"],"type":"object"},"InvocationArgumentDescriptor":{"additionalProperties":false,"properties":{"completion":{"$ref":"#/$defs/InvocationCompletion","default":{"kind":"none"}},"description":{"type":["string","null"]},"name":{"type":"string"},"position":{"description":"Zero-based positional index. `None` means named-only. A positional\nargument may also be supplied by name; this makes chip editing stable.","format":"uint16","maximum":65535.0,"minimum":0.0,"type":["integer","null"]},"required":{"default":false,"type":"boolean"},"value_type":{"$ref":"#/$defs/InvocationArgumentType"}},"required":["name","value_type","completion","required"],"type":"object"},"InvocationArgumentType":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"string","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"integer","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"boolean","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"enum","type":"string"},"values":{"items":{"type":"string"},"type":"array"}},"required":["kind","values"],"type":"object"},{"additionalProperties":false,"description":"A Worker-readable path. This does not grant filesystem authority.","properties":{"kind":{"const":"worker_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"description":"A resource selected and staged by the client. It must never be resolved\nas a path on the Worker host.","properties":{"kind":{"const":"client_file","type":"string"}},"required":["kind"],"type":"object"}]},"InvocationArgumentValue":{"additionalProperties":false,"properties":{"name":{"type":"string"},"value":{"$ref":"#/$defs/InvocationValue"}},"required":["name","value"],"type":"object"},"InvocationClientAdapter":{"oneOf":[{"const":"attachment","description":"The client stages a local file and replaces the draft invocation with an\n`UploadedFile` chip. Local paths are not sent to or persisted by Worker.","type":"string"}]},"InvocationCompletion":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"static","type":"string"},"values":{"items":{"type":"string"},"type":"array"}},"required":["kind","values"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"worker_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"client_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"description":"Host-owned provider selected by source-qualified id. Metadata lookup is\nside-effect free; implementations never cross this wire boundary.","properties":{"kind":{"const":"provider","type":"string"},"provider":{"type":"string"}},"required":["kind","provider"],"type":"object"}]},"InvocationValue":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"string","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"integer","type":"string"},"value":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"boolean","type":"string"},"value":{"type":"boolean"}},"required":["kind","value"],"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":{"submission_id":{"type":"string"}},"required":["submission_id"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Remove exactly the observed ordered set of submissions, preserving the\nactive run. Reject if the FIFO content changed before admission.","properties":{"method":{"const":"clear_pending_submissions","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_submission_ids":{"items":{"type":"string"},"type":"array"}},"required":["expected_submission_ids"],"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"}},"required":["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":"Rewind to just before a user input identified by its public Session entry ID.\nValidate the target against the current segment at execution time.\nLater appended history is discarded too; tool side effects are not undone.","properties":{"method":{"const":"rewind_to","type":"string"},"params":{"additionalProperties":false,"properties":{"entry_id":{"type":"string"}},"required":["entry_id"],"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":{"context":{"anyOf":[{"$ref":"#/$defs/CompletionContext"},{"type":"null"}]},"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"type":"string"},"request_id":{"description":"Client query identity. Echoed verbatim so identical-prefix ABA\nresponses cannot cross edits, target switches, or permission changes.","type":["string","null"]}},"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":"Session-owned FIFO content 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"},"submissions":{"default":[],"items":{"$ref":"#/$defs/PendingSubmissionSummary"},"type":"array"}},"required":["head_id","notification_count","submissions"],"type":"object"},"Permission":{"description":"Permission lattice used by [`ScopeRule`].\n\nThe derived `Ord` instance follows declaration order, so\n`Read < Write`. Allow rules grant the stated level (and by extension\neverything below); deny rules cap the effective level **strictly\nbelow** the stated level.","enum":["read","write"],"type":"string"},"ReasoningConfig":{"description":"Public reasoning configuration resolved for the model client.\n\nThis deliberately exposes only the effective provider control, not the\nWorker manifest or provider credentials. The tagged shape prevents token\nbudgets from being presented as effort labels.","oneOf":[{"additionalProperties":false,"properties":{"effort":{"type":"string"},"kind":{"const":"effort","type":"string"}},"required":["kind","effort"],"type":"object"},{"additionalProperties":false,"properties":{"budget_tokens":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"},"kind":{"const":"budget_tokens","type":"string"}},"required":["kind","budget_tokens"],"type":"object"}]},"RewindSummary":{"additionalProperties":false,"properties":{"discarded_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_side_effect_warning":{"type":"boolean"},"truncated_to_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["truncated_to_entries","discarded_entries","tool_side_effect_warning"],"type":"object"},"RunFailureKind":{"enum":["engine","compaction"],"type":"string"},"RunResult":{"oneOf":[{"enum":["finished","paused","limit_reached"],"type":"string"},{"const":"cancelled","description":"The logical Run ended because the user intentionally cancelled it after\nobservable work. Unlike `RolledBack`, committed history is retained.","type":"string"},{"const":"rolled_back","description":"The accepted Method::Submit produced no assistant/tool output before\nuser interruption, so the Worker rolled the submit-time turn state back\nto its pre-submit snapshot. Clients should treat the Worker as Idle and\nrestore the just-submitted input into the editable composer if desired.","type":"string"}]},"RunResumeSource":{"enum":["compaction","pause"],"type":"string"},"RunYieldReason":{"enum":["compaction"],"type":"string"},"ScopeRule":{"additionalProperties":false,"description":"A single allow or deny rule inside a scope configuration.","properties":{"permission":{"$ref":"#/$defs/Permission","description":"Permission level this rule grants (allow) or caps strictly below\n(deny)."},"recursive":{"default":true,"description":"When `false`, the rule only matches the target itself and its\ndirect children. Defaults to `true`.","type":"boolean"},"symlink_policy":{"$ref":"#/$defs/SymlinkPolicy","default":"resolved","description":"Which path identity an allow rule uses when symbolic links are\nencountered. Deny rules always inspect both identities."},"target":{"description":"Target path. Must be absolute by the time a `Scope` is built from\nthis rule — relative paths are resolved per-layer against the\nmanifest file's directory (cwd for overlay layers) before cascade\nmerge.","type":"string"}},"required":["target","permission","recursive","symlink_policy"],"type":"object"},"Segment":{"oneOf":[{"additionalProperties":false,"description":"Free-form text. The fallback every client can produce.","properties":{"content":{"type":"string"},"kind":{"const":"text","type":"string"}},"required":["kind","content"],"type":"object"},{"additionalProperties":false,"description":"Bracketed-paste capture from a TUI-style client. `id`, `chars`\nand `lines` carry the metadata needed to re-render a\n`[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage`\nre-broadcast.","properties":{"chars":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"content":{"type":"string"},"id":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"kind":{"const":"paste","type":"string"},"lines":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"}},"required":["kind","id","chars","lines","content"],"type":"object"},{"additionalProperties":false,"description":"Internal reference produced when Worker stores a large `Paste` before\ncommitting input. Clients may receive this in history/event projections;\nthe body is intentionally absent.","properties":{"artifact":{"$ref":"#/$defs/PasteArtifactRef"},"kind":{"const":"paste_artifact","type":"string"}},"required":["kind","artifact"],"type":"object"},{"additionalProperties":false,"description":"Client-local file uploaded into the owning Worker session before submit.\nThe Session Log stores only this immutable reference, never file bytes or\nthe client's local path.","properties":{"file":{"$ref":"#/$defs/UploadedFileRef"},"kind":{"const":"uploaded_file","type":"string"}},"required":["kind","file"],"type":"object"},{"additionalProperties":false,"description":"`@` file-system reference. Worker resolves readable files to\n`[File: ]` attachments and readable normal directories to shallow\n`[Dir: ]` listings; the flattened user text keeps the literal\n`@` placeholder either way.","properties":{"kind":{"const":"file_ref","type":"string"},"path":{"type":"string"}},"required":["kind","path"],"type":"object"},{"additionalProperties":false,"description":"Explicitly selected, source-qualified Feature invocation. The host never\npromotes free text to this variant and validates it against the installed\ndeclaration before execution.","properties":{"invocation":{"$ref":"#/$defs/FeatureInvocation"},"kind":{"const":"feature_invoke","type":"string"}},"required":["kind","invocation"],"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,"submissions":[]}}},"required":["entries","pending_submissions"],"type":"object"},"SessionSnapshotEntry":{"oneOf":[{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"user_input","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","segments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"items":{"$ref":"#/$defs/SessionContentPart"},"type":"array"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"message","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"role":{"$ref":"#/$defs/SessionMessageRole"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","role","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"call_id":{"type":"string"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","name","arguments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"attachments":{"items":{"$ref":"#/$defs/SessionToolAttachment"},"type":"array"},"call_id":{"type":"string"},"content":{"type":["string","null"]},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"is_error":{"type":"boolean"},"kind":{"const":"tool_result","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"summary":{"type":"string"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","summary","is_error","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"type":"string"},"data":true,"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"item_kind":{"type":"string"},"kind":{"const":"system_item","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","item_kind","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable, non-terminal control return from one logical Run. This is not a\ncompleted run and must not be used to infer the current Worker state.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_yielded","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"reason":{"$ref":"#/$defs/RunYieldReason"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","reason","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable boundary immediately before Engine execution resumes. The source\ndistinguishes automatic compaction continuation from an intentional\nuser-pause resume; it does not prove that later model output occurred.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_resumed","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"source":{"$ref":"#/$defs/RunResumeSource"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","source","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Snapshot-only replay of persisted invocation/usage records. These are\naccounting metadata, never model context or conversation rows.","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":"invoke","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"},"trigger":{"$ref":"#/$defs/InvokeKind"}},"required":["kind","trigger","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"cache_read_input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"kind":{"const":"usage","type":"string"},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"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","input_tokens","cache_read_input_tokens","output_tokens","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Display-only accounting reconstructed from persisted run/usage records.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"elapsed_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"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_stats","type":"string"},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"requests":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"upload_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","elapsed_ms","requests","upload_tokens","output_tokens","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable terminal for an intentional cancellation. Consumers must not\npresent this lifecycle result as an execution failure.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_cancelled","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"failure":{"anyOf":[{"$ref":"#/$defs/RunFailureKind"},{"type":"null"}]},"kind":{"const":"run_error","type":"string"},"message":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","message","entry_id","timestamp","provenance"],"type":"object"}]},"SessionToolAttachment":{"additionalProperties":false,"properties":{"attachment_id":{"description":"Stable, bounded identity derived from the owning Session entry, attachment\nposition, and immutable image bytes. It is a locator, not a bearer secret.","type":"string"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"media_type":{"type":"string"}},"required":["attachment_id","media_type","byte_len"],"type":"object"},"SubmissionDisposition":{"description":"Durable acceptance result for one idempotent Submit request.","oneOf":[{"enum":["started"],"type":"string"},{"const":"queued","description":"Input is durably held in the session FIFO until eligible for activation.","type":"string"}]},"SubscriptionEvent":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"payload":{"$ref":"#/$defs/SubscriptionEventPayload"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","payload"],"type":"object"},"event":{"const":"event","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionTerminationCode"},"message":{"type":"string"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","code","message"],"type":"object"},"event":{"const":"subscription_closed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionEventPayload":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"worker":{"$ref":"#/$defs/SubscriptionWorker"}},"required":["worker"],"type":"object"},"event":{"const":"worker_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id"],"type":"object"},"event":{"const":"worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","event"],"type":"object"},"event":{"const":"worker_protocol","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdir":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"}},"required":["workdir"],"type":"object"},"event":{"const":"workdir_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id"],"type":"object"},"event":{"const":"workdir_removed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionId":{"type":"string"},"SubscriptionRejectionCode":{"enum":["invalid_request","unsupported_protocol_version","unsupported_selector","unauthorized","resource_not_found","capacity_exceeded","internal"],"type":"string"},"SubscriptionRequest":{"oneOf":[{"additionalProperties":false,"properties":{"method":{"const":"subscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"}},"required":["request_id","selector"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"unsubscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"}},"required":["method","params"],"type":"object"}]},"SubscriptionRequestId":{"type":"string"},"SubscriptionResponse":{"oneOf":[{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"},"snapshot":{"$ref":"#/$defs/SubscriptionSnapshot"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id","selector","snapshot"],"type":"object"},"result":{"const":"subscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"},"result":{"const":"unsubscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionRejectionCode"},"message":{"type":"string"},"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"anyOf":[{"$ref":"#/$defs/SubscriptionId"},{"type":"null"}]}},"required":["request_id","code","message"],"type":"object"},"result":{"const":"subscription_rejected","type":"string"}},"required":["result","payload"],"type":"object"}]},"SubscriptionSnapshot":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workers":{"items":{"$ref":"#/$defs/SubscriptionWorker"},"type":"array"}},"required":["workers"],"type":"object"},"topic":{"const":"workers","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"events":{"items":{"$ref":"#/$defs/Event"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","events"],"type":"object"},"topic":{"const":"worker_protocol","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdirs":{"items":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"},"type":"array"}},"required":["workdirs"],"type":"object"},"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic","data"],"type":"object"}]},"SubscriptionTerminationCode":{"enum":["lagged","resource_gone","unauthorized","server_shutdown"],"type":"string"},"SubscriptionWorkdirId":{"type":"string"},"SubscriptionWorker":{"additionalProperties":false,"properties":{"availability":{"$ref":"#/$defs/SubscriptionWorkerAvailability","default":"observed","description":"Freshness of the Runtime-backed observation carried by this projection.\n`Unavailable` preserves catalog membership without claiming that execution stopped."},"display_name":{"type":["string","null"]},"has_running_internal_workers":{"default":false,"type":"boolean"},"job":{"anyOf":[{"$ref":"#/$defs/SubscriptionWorkerJob"},{"type":"null"}],"description":"Backend-owned Job binding. Runtime producers leave this unset; the\nWorkspace projection enriches both snapshots and live updates."},"profile":{"type":["string","null"]},"resource_key":{"description":"Workspace-scoped canonical resource key. Runtime producers leave this unset;\nWorkspace-facing projections must populate it before publishing the Worker.","type":["string","null"]},"restore_observation_token":{"description":"Opaque Runtime-owned Restore observation identity, never a display-state hash.","type":["string","null"]},"runtime_id":{"description":"Set by the Workspace Server when projecting a Runtime-owned Worker to clients.\nRuntime producers leave this unset because the connection identifies the Runtime.","type":["string","null"]},"state":{"$ref":"#/$defs/SubscriptionWorkerState","description":"Runtime catalog lifecycle compatibility projection; not foreground-state authority."},"workdir_attachments":{"items":{"$ref":"#/$defs/SubscriptionWorkerWorkdirAttachment"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"},"worker_state":{"anyOf":[{"$ref":"#/$defs/WorkerStateSnapshot"},{"type":"null"}],"description":"Latest foreground state observed from the Worker. This remains\nabsent until an authoritative Worker snapshot/event has been applied."},"workspace_id":{"type":["string","null"]}},"required":["worker_id","state","availability","has_running_internal_workers"],"type":"object"},"SubscriptionWorkerAvailability":{"description":"Whether the Workspace Server has a current Runtime observation for a Worker.\n\n`Unavailable` deliberately does not imply `Stopped`: it means the last observed\nlifecycle state is retained while the Runtime observation source is unavailable.","enum":["observed","unavailable"],"type":"string"},"SubscriptionWorkerId":{"type":"string"},"SubscriptionWorkerIds":{"description":"A bounded, canonical worker-id set. Ordering on the wire and in equality/hash\nsemantics is lexical, so equivalent selectors aggregate to one upstream key.","items":{"$ref":"#/$defs/SubscriptionWorkerId"},"type":"array"},"SubscriptionWorkerJob":{"additionalProperties":false,"properties":{"attempt_id":{"type":"string"},"job_id":{"type":"string"},"purpose":{"type":"string"}},"required":["job_id","attempt_id","purpose"],"type":"object"},"SubscriptionWorkerProtocolMethod":{"additionalProperties":false,"properties":{"method":{"$ref":"#/$defs/Method"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","method"],"type":"object"},"SubscriptionWorkerState":{"enum":["idle","running","paused","stopped"],"type":"string"},"SubscriptionWorkerWorkdirAttachment":{"additionalProperties":false,"properties":{"alias":{"description":"Stable Worker-local routing alias.","type":"string"},"repository_key":{"type":["string","null"]},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["alias","working_directory_id"],"type":"object"},"SymlinkPolicy":{"description":"Symbolic-link identity used by one filesystem allow rule.\n\n`Resolved` is the least authority and the default: access is matched\nagainst the provider-resolved target. `Logical` intentionally grants the\npath as presented through the Workdir, even when it aliases another target.","enum":["resolved","logical"],"type":"string"},"ToolResultDisposition":{"enum":["success","error","interrupted","cancelled","outcome_unknown"],"type":"string"},"TurnResult":{"enum":["finished","paused"],"type":"string"},"UploadedFileAvailability":{"description":"Availability recorded for an uploaded client-local file.","enum":["available","unavailable","integrity_failed"],"type":"string"},"UploadedFileRef":{"additionalProperties":false,"description":"Session-owned immutable reference to a client-local uploaded file.\n\nUpload transports return an unbound reference. Worker fills\n`source_entry_id` immediately before the containing user input is committed;\ncommitted Session Log and public snapshot records therefore always retain\nthe durable source-entry identity without storing the file body.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/UploadedFileAvailability"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"file_name":{"type":"string"},"media_type":{"type":"string"},"sha256":{"type":"string"},"source_entry_id":{"type":["string","null"]}},"required":["artifact_id","file_name","media_type","created_at_ms","availability","byte_len","sha256"],"type":"object"},"WorkerBusyState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"run","type":"string"},"state":{"$ref":"#/$defs/WorkerRunState"}},"required":["kind","state"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"maintenance","type":"string"},"state":{"$ref":"#/$defs/WorkerMaintenanceState"}},"required":["kind","state"],"type":"object"}]},"WorkerCommandAcknowledgement":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandKind"},"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"disposition":{"$ref":"#/$defs/WorkerCommandDisposition"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"The complete authoritative state observed after command admission."}},"required":["command_id","command","disposition","state"],"type":"object"},"WorkerCommandDisposition":{"enum":["accepted","stale_command_id","conflict","invalid_state"],"type":"string"},"WorkerCommandEnvelope":{"additionalProperties":false,"description":"Caller-owned identity for one state-changing Worker command.\n\nCommands operate on the controller state at admission time, not a caller's\npreviously observed Run. A controller accepts command ids in strictly\nincreasing order; retrying an accepted id cannot apply it to a later Run.\nExact retries must retain the same command kind. Removed preconditions are\nrejected at the wire boundary rather than treated as arrival-time commands.","properties":{"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["command_id"],"type":"object"},"WorkerCommandKind":{"enum":["resume","cancel","pause","compact","shutdown"],"type":"string"},"WorkerEvent":{"description":"Typed lifecycle events sent from a child Worker to its parent.\n\nDelivered as `Method::WorkerEvent` over the parent's Unix socket. The\nparent Controller always applies variant-specific side effects\n(registry / worker-allocation updates). Agent-visible variants are also\nqueued into the notification buffer; control-plane-only variants are\nnot injected into the parent's LLM context.\n\nTransport is fire-and-forget; receivers must tolerate out-of-order\ndelivery (e.g. `TurnEnded` arriving after `ShutDown` for the same\nchild Worker).","oneOf":[{"additionalProperties":false,"description":"Child finished one turn and is back to IDLE.","properties":{"kind":{"const":"turn_ended","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Engine execution error occurred inside the child's turn.\n\nLimited to worker runtime failures (provider / tool errors) —\ndoes not include transient method-rejection responses such as\n`AlreadyRunning`.","properties":{"kind":{"const":"errored","type":"string"},"message":{"type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name","message"],"type":"object"},{"additionalProperties":false,"description":"Child has stopped (controller loop is exiting).","properties":{"kind":{"const":"shut_down","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Child SubWorker sub-delegated scope to a grandchild SubWorker via `SubWorkerSpawn`.\n\nControl-plane only: receivers apply registry side effects and\npropagate upward, but do not expose this as an agent notification.\n\nThe parent uses this to add the grandchild to its own\n`spawned_workers.json` so it can manage the grandchild directly\neven if the intermediate child dies. The parent then re-fires\nthis event upward (if it has a parent of its own) to maintain\nthe chain to root.","properties":{"kind":{"const":"scope_sub_delegated","type":"string"},"parent_worker":{"description":"Sub-delegating Worker (= the sender itself).","type":"string"},"scope":{"description":"Scope delegated to the grandchild.","items":{"$ref":"#/$defs/ScopeRule"},"type":"array"},"sub_socket":{"description":"Unix-socket path where the grandchild is reachable.","type":"string"},"sub_worker":{"description":"Name of the grandchild Worker.","type":"string"}},"required":["kind","parent_worker","sub_worker","sub_socket","scope"],"type":"object"}]},"WorkerMaintenanceState":{"enum":["compacting"],"type":"string"},"WorkerRunState":{"enum":["running","pausing","paused","cancelling"],"type":"string"},"WorkerState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"idle","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"busy","type":"string"},"state":{"$ref":"#/$defs/WorkerBusyState"}},"required":["kind","state"],"type":"object"}]},"WorkerStateSnapshot":{"additionalProperties":false,"properties":{"last_command_id":{"description":"Highest lifecycle command id observed by this controller instance.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"last_finished_submission_request_id":{"description":"Most recent Submit request whose controller Run has returned and whose\npost-Run state transition has been published. This is a teardown fence,\nnot a successful-result claim; cancelled and failed Runs may also finish.","type":["string","null"]},"state":{"$ref":"#/$defs/WorkerState"}},"required":["last_command_id","state"],"type":"object"},"WorkerStatus":{"enum":["idle","running","paused","stopped"],"type":"string"},"WorkspaceSubscriptionWorkdir":{"additionalProperties":false,"description":"Workspace-facing Workdir summary. Backend-generated Repository UUIDs never\nenter this DTO; Workspace Server must resolve the required Repository key.","properties":{"repository_key":{"type":"string"},"state":{"type":"string"},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id","repository_key","state"],"type":"object"}},"$schema":"https://json-schema.org/draft/2020-12/schema","description":"Direction is explicit at the outer envelope, while concrete message kinds\nremain typed within each lane.","oneOf":[{"additionalProperties":false,"properties":{"frame":{"const":"request","type":"string"},"message":{"$ref":"#/$defs/SubscriptionRequest"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"response","type":"string"},"message":{"$ref":"#/$defs/SubscriptionResponse"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"event","type":"string"},"message":{"$ref":"#/$defs/SubscriptionEvent"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"worker_protocol","type":"string"},"message":{"$ref":"#/$defs/SubscriptionWorkerProtocolMethod"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"}],"title":"SubscriptionFrame"}; +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":{"id":{"description":"Stable only within the emitting live Worker; not durable alert history.","type":["string","null"]},"level":{"$ref":"#/$defs/AlertLevel"},"message":{"type":"string"},"position":{"anyOf":[{"$ref":"#/$defs/AlertPosition"},{"type":"null"}]},"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"},"AlertPosition":{"description":"Display anchor captured at emission, resolved when pending text is committed.","oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"start","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"entry_id":{"type":"string"},"kind":{"const":"after_entry","type":"string"}},"required":["kind","entry_id"],"type":"object"},{"additionalProperties":false,"properties":{"call_id":{"type":"string"},"kind":{"const":"after_tool_call","type":"string"}},"required":["kind","call_id"],"type":"object"},{"additionalProperties":false,"properties":{"block_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"kind":{"const":"after_in_flight","type":"string"}},"required":["kind","block_id"],"type":"object"}]},"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"]},"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","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"},"CompletionContext":{"additionalProperties":false,"properties":{"argument":{"type":["string","null"]},"invocation":{"$ref":"#/$defs/FeatureInvocationIdentity"}},"required":["invocation"],"type":"object"},"CompletionEntry":{"additionalProperties":false,"description":"One completion candidate for a prefix query.","properties":{"description":{"type":["string","null"]},"invocation":{"anyOf":[{"$ref":"#/$defs/FeatureInvocationDescriptor"},{"type":"null"}]},"is_dir":{"default":false,"type":"boolean"},"usage":{"type":["string","null"]},"value":{"type":"string"}},"required":["value","is_dir"],"type":"object"},"CompletionKind":{"description":"Kind of completion requested by `Method::ListCompletions`.\n\n`File` is the existing `@` path lane. `Feature` lists installed chat\ninvocations. `FeatureArgument` resolves a declared argument using the\nsource-qualified [`CompletionContext`].","enum":["file","feature","feature_argument"],"type":"string"},"ContextTokenSource":{"description":"Provenance of the current-context token count in a [`Greeting`].","oneOf":[{"const":"measured","description":"Provider-reported input occupancy from the most recent LLM request.","type":"string"},{"const":"estimated","description":"Current history projected from the Worker's token accounting records.","type":"string"}]},"ContextUsage":{"additionalProperties":false,"description":"Current prompt-context occupancy, distinct from cumulative run traffic.","properties":{"source":{"$ref":"#/$defs/ContextTokenSource"},"tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["tokens","source"],"type":"object"},"ErrorCode":{"enum":["already_running","not_running","not_paused","provider_error","tool_error","invalid_request","internal"],"type":"string"},"Event":{"oneOf":[{"additionalProperties":false,"description":"Durable Submit acceptance. A `Queued` receipt follows the durable FIFO\ncheckpoint. A `Started` receipt follows the pending activation checkpoint;\nthe corresponding [`Event::UserMessage`] marks the later history commit.\nRepeating the same request id and exact payload returns the same receipt\nwithout appending or activating twice.","properties":{"data":{"additionalProperties":false,"properties":{"disposition":{"$ref":"#/$defs/SubmissionDisposition"},"submission_id":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","submission_id","disposition"],"type":"object"},"event":{"const":"submission_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"submission_request_id":{"type":"string"}},"required":["submission_request_id","message"],"type":"object"},"event":{"const":"submission_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Durable notification acceptance. The request identity is persisted with\nthe notification receipt before this event is emitted. Repeating the\nsame request id and exact payload returns the same acknowledgement\nwithout appending the notification twice.","properties":{"data":{"additionalProperties":false,"properties":{"notification_request_id":{"type":"string"}},"required":["notification_request_id"],"type":"object"},"event":{"const":"notification_accepted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Correlated notification rejection before durable acceptance.","properties":{"data":{"additionalProperties":false,"properties":{"message":{"type":"string"},"notification_request_id":{"type":"string"}},"required":["notification_request_id","message"],"type":"object"},"event":{"const":"notification_rejected","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Complete FIFO replacement following enqueue, activation, cancel, or clear.\nClients replace their pending view in stream order; reconnect uses the snapshot.","properties":{"data":{"additionalProperties":false,"properties":{"pending":{"$ref":"#/$defs/PendingSubmissionsSnapshot"}},"required":["pending"],"type":"object"},"event":{"const":"pending_submissions_changed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A user input message persisted as `LogEntry::AnnotatedUserInput` and\nactivated for a turn. Broadcast to every subscribed client so TUI / GUI\ninstances show the same user line that reconnect snapshots replay.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Older mixed-version producers omit it.","type":["string","null"]},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"}},"required":["segments"],"type":"object"},"event":{"const":"user_message","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A canonical assistant/tool entry has committed to durable history.\nStreaming events remain the in-flight authority; this event supplies the\nstable identity used to reconcile that live block with paged history.","properties":{"data":{"additionalProperties":false,"properties":{"entry":{"$ref":"#/$defs/SessionSnapshotEntry"}},"required":["entry"],"type":"object"},"event":{"const":"session_entry_committed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"One agent-injected system item committed to history.\n\nCarries the JSON form of `session_store::SystemItem`. Covers\n`Method::Notify` echoes, child-Worker lifecycle events from\n`Method::WorkerEvent`, `@` resolution payloads, and any future\nClients dispatch on the `kind` tag for typed rendering instead\nof parsing free-text prefixes like `[Notification] …` or\n`[File: …]`.\n\nOne event per `LogEntry::AnnotatedSystemItem` commit. Disk-side and\nwire-side are 1:1.","properties":{"data":{"additionalProperties":false,"properties":{"entry_id":{"description":"Stable durable history identity. Synthetic compatibility events omit it.","type":["string","null"]},"item":true},"required":["item"],"type":"object"},"event":{"const":"system_item","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"A new self-driving cycle has begun (IDLE → active transition).\n\nMarker event for the start of an Invoke range; the range extends\nimplicitly until the next `InvokeStart`. Fires for every accepted\n`Method::Submit` (kind=`UserSend`), `Method::Notify` (kind=`Notify`),\n`Method::WorkerEvent` re-injection (kind=`WorkerEvent`), and any other\nIDLE-breaking trigger. Mid-run interrupts (e.g. hook output,\ntyped system reminder insertion that doesn't break IDLE) do not\nemit `InvokeStart` — they appear as `SystemItem` only.\n\nCarries `kind` only; the payload (user text / notify message /\nworker event body) is delivered separately via the immediately\nfollowing `UserMessage` / `SystemItem` event.","properties":{"data":{"additionalProperties":false,"properties":{"kind":{"$ref":"#/$defs/InvokeKind"},"timestamp_ms":{"description":"Timestamp of the persisted Invoke, not the client's attach time.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"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"]},"timestamp_ms":{"description":"Present for usage delivered from the committed session-log lane.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":["integer","null"]}},"type":"object"},"event":{"const":"usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Authoritative current-context occupancy after a non-request boundary.\n\nUnlike [`Event::Usage`], this is not cumulative request traffic. It lets\nconnected clients converge with reconnect snapshots after run completion,\ncompaction, rewind, and other history rewrites. `None` explicitly means\nthe current value is unavailable and stale occupancy must be cleared.","properties":{"data":{"additionalProperties":false,"properties":{"usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}]}},"type":"object"},"event":{"const":"context_usage","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"result":{"$ref":"#/$defs/RunResult"}},"required":["result"],"type":"object"},"event":{"const":"run_end","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/ErrorCode"},"message":{"type":"string"}},"required":["code","message"],"type":"object"},"event":{"const":"error","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"description":"Sent exactly once at the start of every client connection.\n\n`entries` is the session-log mirror at subscribe time, serialised\nas the JSON form of `session_store::LogEntry`. This is the\nbulk-reconstruction lane: clients walk the entries to seed their\nderived view.\n\n`greeting` and `status` accompany the snapshot so clients render\nworker identity and current controller state without an extra round\ntrip.\n\nLive updates after the snapshot arrive through the streaming\nevents (`TextDelta` / `ToolCall*` / `ToolResult` / etc.) plus\nrole-specific entry events (`SegmentRotated` / `SystemItem`) —\nthere is no generic \"every committed entry\" broadcast.","properties":{"data":{"additionalProperties":false,"properties":{"greeting":{"$ref":"#/$defs/Greeting"},"in_flight":{"$ref":"#/$defs/InFlightSnapshot","description":"Unfinished model output that has already streamed in the current\nrun but is not yet represented by committed snapshot entries."},"internal_workers":{"description":"Parent-owned Internal Worker sessions visible to this client.\nService-private Internal Workers are deliberately excluded.","items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"Full 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. Apply in arrival order on the parent's serialized stream; a\nreconnect snapshot replaces the projection before live events resume.","properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","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":{"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker"],"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":{"context":{"anyOf":[{"$ref":"#/$defs/CompletionContext"},{"type":"null"}]},"entries":{"items":{"$ref":"#/$defs/CompletionEntry"},"type":"array"},"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"default":"","type":"string"},"request_id":{"type":["string","null"]}},"required":["kind","entries","prefix"],"type":"object"},"event":{"const":"completions","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"}]},"FeatureInvocation":{"additionalProperties":false,"properties":{"arguments":{"items":{"$ref":"#/$defs/InvocationArgumentValue"},"type":"array"},"identity":{"$ref":"#/$defs/FeatureInvocationIdentity"},"invocation_id":{"description":"Stable client-generated id retained across queueing and transport retry.","type":"string"},"name":{"description":"Selected public name, retained for exact draft/history restoration. Host\nresolution is always by `identity`.","type":"string"}},"required":["invocation_id","identity","name","arguments"],"type":"object"},"FeatureInvocationDescriptor":{"additionalProperties":false,"properties":{"aliases":{"default":[],"items":{"type":"string"},"type":"array"},"arguments":{"default":[],"items":{"$ref":"#/$defs/InvocationArgumentDescriptor"},"type":"array"},"client_adapter":{"anyOf":[{"$ref":"#/$defs/InvocationClientAdapter"},{"type":"null"}]},"description":{"type":"string"},"display_name":{"type":"string"},"identity":{"$ref":"#/$defs/FeatureInvocationIdentity"},"name":{"type":"string"},"syntax":{"$ref":"#/$defs/FeatureInvocationSyntax"}},"required":["identity","name","display_name","description","syntax","aliases","arguments"],"type":"object"},"FeatureInvocationIdentity":{"description":"Source-qualified identity of one invocation contribution, for example\n`builtin:attachments/attach`. Display names and aliases are never authority.","type":"string"},"FeatureInvocationSyntax":{"oneOf":[{"const":"parenthesized","description":"`/name(value, key=value)` with JSON-style quoted strings and `)` as the\nexplicit boundary before following natural language.","type":"string"}]},"Greeting":{"additionalProperties":false,"description":"Worker self-description rendered by clients when a session starts empty.\n\nSnapshot metadata contains only resolved public execution information so\nclients do not need their own copy of the manifest.","properties":{"context_tokens":{"default":0,"description":"Legacy numeric mirror retained for mixed-version clients.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"context_usage":{"anyOf":[{"$ref":"#/$defs/ContextUsage"},{"type":"null"}],"description":"Typed current-context value. Older snapshots omit it; clients may fall\nback to `context_tokens` only when `context_window` is known."},"context_window":{"default":0,"description":"Resolved effective model context window in tokens. Legacy producers use\nzero when this is unavailable; clients must treat that as unknown.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"cwd":{"type":"string"},"model":{"type":"string"},"provider":{"type":"string"},"reasoning":{"anyOf":[{"$ref":"#/$defs/ReasoningConfig"},{"type":"null"}],"description":"Effective reasoning control. Absent means unspecified or unavailable."},"scope_summary":{"type":"string"},"tools":{"items":{"type":"string"},"type":"array"},"worker_name":{"type":"string"}},"required":["worker_name","cwd","provider","model","scope_summary","tools","context_tokens","context_window"],"type":"object"},"InFlightBlock":{"oneOf":[{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"text","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"finished":{"type":"boolean"},"kind":{"const":"thinking","type":"string"},"text":{"type":"string"}},"required":["kind","text"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"type":"string"},"id":{"type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"state":{"$ref":"#/$defs/InFlightToolCallState"}},"required":["kind","id","name","args"],"type":"object"}]},"InFlightCompaction":{"additionalProperties":false,"properties":{"phase":{"$ref":"#/$defs/CompactionPhase"},"started_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"trigger":{"$ref":"#/$defs/CompactionTrigger"}},"required":["phase","started_at_ms","trigger"],"type":"object"},"InFlightSnapshot":{"additionalProperties":false,"description":"Unfinished model output and active command state included in\n`Event::Snapshot` for clients that attach while work is still streaming.\n\nThese blocks are presentation state only: they are reconstructed from the\nactive Worker controller and must not be treated as committed assistant\nhistory. Finalized assistant items continue to come from ordinary snapshot\nentries.","properties":{"block_ids":{"description":"Live-Worker identities parallel to the blocks actually included below.","items":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"type":"array"},"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 parent serializes\nsnapshot publication and subsequent live events on the same ordered stream.","properties":{"error":{"type":["string","null"]},"greeting":{"anyOf":[{"$ref":"#/$defs/Greeting"},{"type":"null"}],"description":"Public execution metadata for this child. Older snapshots and service\nworkers may omit it; clients must never substitute the parent's values."},"in_flight":{"$ref":"#/$defs/InFlightSnapshot"},"internal_workers":{"items":{"$ref":"#/$defs/InternalWorkerSnapshot"},"type":"array"},"session":{"$ref":"#/$defs/SessionSnapshot"},"status":{"$ref":"#/$defs/WorkerStatus","default":"idle"},"worker":{"$ref":"#/$defs/InternalWorkerRef"}},"required":["worker","session","status"],"type":"object"},"InvocationArgumentDescriptor":{"additionalProperties":false,"properties":{"completion":{"$ref":"#/$defs/InvocationCompletion","default":{"kind":"none"}},"description":{"type":["string","null"]},"name":{"type":"string"},"position":{"description":"Zero-based positional index. `None` means named-only. A positional\nargument may also be supplied by name; this makes chip editing stable.","format":"uint16","maximum":65535.0,"minimum":0.0,"type":["integer","null"]},"required":{"default":false,"type":"boolean"},"value_type":{"$ref":"#/$defs/InvocationArgumentType"}},"required":["name","value_type","completion","required"],"type":"object"},"InvocationArgumentType":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"string","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"integer","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"boolean","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"enum","type":"string"},"values":{"items":{"type":"string"},"type":"array"}},"required":["kind","values"],"type":"object"},{"additionalProperties":false,"description":"A Worker-readable path. This does not grant filesystem authority.","properties":{"kind":{"const":"worker_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"description":"A resource selected and staged by the client. It must never be resolved\nas a path on the Worker host.","properties":{"kind":{"const":"client_file","type":"string"}},"required":["kind"],"type":"object"}]},"InvocationArgumentValue":{"additionalProperties":false,"properties":{"name":{"type":"string"},"value":{"$ref":"#/$defs/InvocationValue"}},"required":["name","value"],"type":"object"},"InvocationClientAdapter":{"oneOf":[{"const":"attachment","description":"The client stages a local file and replaces the draft invocation with an\n`UploadedFile` chip. Local paths are not sent to or persisted by Worker.","type":"string"}]},"InvocationCompletion":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"static","type":"string"},"values":{"items":{"type":"string"},"type":"array"}},"required":["kind","values"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"worker_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"client_file","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"description":"Host-owned provider selected by source-qualified id. Metadata lookup is\nside-effect free; implementations never cross this wire boundary.","properties":{"kind":{"const":"provider","type":"string"},"provider":{"type":"string"}},"required":["kind","provider"],"type":"object"}]},"InvocationValue":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"string","type":"string"},"value":{"type":"string"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"integer","type":"string"},"value":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"}},"required":["kind","value"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"boolean","type":"string"},"value":{"type":"boolean"}},"required":["kind","value"],"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":{"submission_id":{"type":"string"}},"required":["submission_id"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"description":"Remove exactly the observed ordered set of submissions, preserving the\nactive run. Reject if the FIFO content changed before admission.","properties":{"method":{"const":"clear_pending_submissions","type":"string"},"params":{"additionalProperties":false,"properties":{"expected_submission_ids":{"items":{"type":"string"},"type":"array"}},"required":["expected_submission_ids"],"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"}},"required":["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":"Rewind to just before a user input identified by its public Session entry ID.\nValidate the target against the current segment at execution time.\nLater appended history is discarded too; tool side effects are not undone.","properties":{"method":{"const":"rewind_to","type":"string"},"params":{"additionalProperties":false,"properties":{"entry_id":{"type":"string"}},"required":["entry_id"],"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":{"context":{"anyOf":[{"$ref":"#/$defs/CompletionContext"},{"type":"null"}]},"kind":{"$ref":"#/$defs/CompletionKind"},"prefix":{"type":"string"},"request_id":{"description":"Client query identity. Echoed verbatim so identical-prefix ABA\nresponses cannot cross edits, target switches, or permission changes.","type":["string","null"]}},"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":"Session-owned FIFO content 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"},"submissions":{"default":[],"items":{"$ref":"#/$defs/PendingSubmissionSummary"},"type":"array"}},"required":["head_id","notification_count","submissions"],"type":"object"},"Permission":{"description":"Permission lattice used by [`ScopeRule`].\n\nThe derived `Ord` instance follows declaration order, so\n`Read < Write`. Allow rules grant the stated level (and by extension\neverything below); deny rules cap the effective level **strictly\nbelow** the stated level.","enum":["read","write"],"type":"string"},"ReasoningConfig":{"description":"Public reasoning configuration resolved for the model client.\n\nThis deliberately exposes only the effective provider control, not the\nWorker manifest or provider credentials. The tagged shape prevents token\nbudgets from being presented as effort labels.","oneOf":[{"additionalProperties":false,"properties":{"effort":{"type":"string"},"kind":{"const":"effort","type":"string"}},"required":["kind","effort"],"type":"object"},{"additionalProperties":false,"properties":{"budget_tokens":{"format":"int32","maximum":2147483647.0,"minimum":-2147483648.0,"type":"integer"},"kind":{"const":"budget_tokens","type":"string"}},"required":["kind","budget_tokens"],"type":"object"}]},"RewindSummary":{"additionalProperties":false,"properties":{"discarded_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"tool_side_effect_warning":{"type":"boolean"},"truncated_to_entries":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["truncated_to_entries","discarded_entries","tool_side_effect_warning"],"type":"object"},"RunFailureKind":{"enum":["engine","compaction"],"type":"string"},"RunResult":{"oneOf":[{"enum":["finished","paused","limit_reached"],"type":"string"},{"const":"cancelled","description":"The logical Run ended because the user intentionally cancelled it after\nobservable work. Unlike `RolledBack`, committed history is retained.","type":"string"},{"const":"rolled_back","description":"The accepted Method::Submit produced no assistant/tool output before\nuser interruption, so the Worker rolled the submit-time turn state back\nto its pre-submit snapshot. Clients should treat the Worker as Idle and\nrestore the just-submitted input into the editable composer if desired.","type":"string"}]},"RunResumeSource":{"enum":["compaction","pause"],"type":"string"},"RunYieldReason":{"enum":["compaction"],"type":"string"},"ScopeRule":{"additionalProperties":false,"description":"A single allow or deny rule inside a scope configuration.","properties":{"permission":{"$ref":"#/$defs/Permission","description":"Permission level this rule grants (allow) or caps strictly below\n(deny)."},"recursive":{"default":true,"description":"When `false`, the rule only matches the target itself and its\ndirect children. Defaults to `true`.","type":"boolean"},"symlink_policy":{"$ref":"#/$defs/SymlinkPolicy","default":"resolved","description":"Which path identity an allow rule uses when symbolic links are\nencountered. Deny rules always inspect both identities."},"target":{"description":"Target path. Must be absolute by the time a `Scope` is built from\nthis rule — relative paths are resolved per-layer against the\nmanifest file's directory (cwd for overlay layers) before cascade\nmerge.","type":"string"}},"required":["target","permission","recursive","symlink_policy"],"type":"object"},"Segment":{"oneOf":[{"additionalProperties":false,"description":"Free-form text. The fallback every client can produce.","properties":{"content":{"type":"string"},"kind":{"const":"text","type":"string"}},"required":["kind","content"],"type":"object"},{"additionalProperties":false,"description":"Bracketed-paste capture from a TUI-style client. `id`, `chars`\nand `lines` carry the metadata needed to re-render a\n`[Clipboard #N | X chars, Y lines]` chip in `Event::UserMessage`\nre-broadcast.","properties":{"chars":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"content":{"type":"string"},"id":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"},"kind":{"const":"paste","type":"string"},"lines":{"format":"uint32","maximum":4294967295.0,"minimum":0.0,"type":"integer"}},"required":["kind","id","chars","lines","content"],"type":"object"},{"additionalProperties":false,"description":"Internal reference produced when Worker stores a large `Paste` before\ncommitting input. Clients may receive this in history/event projections;\nthe body is intentionally absent.","properties":{"artifact":{"$ref":"#/$defs/PasteArtifactRef"},"kind":{"const":"paste_artifact","type":"string"}},"required":["kind","artifact"],"type":"object"},{"additionalProperties":false,"description":"Client-local file uploaded into the owning Worker session before submit.\nThe Session Log stores only this immutable reference, never file bytes or\nthe client's local path.","properties":{"file":{"$ref":"#/$defs/UploadedFileRef"},"kind":{"const":"uploaded_file","type":"string"}},"required":["kind","file"],"type":"object"},{"additionalProperties":false,"description":"`@` file-system reference. Worker resolves readable files to\n`[File: ]` attachments and readable normal directories to shallow\n`[Dir: ]` listings; the flattened user text keeps the literal\n`@` placeholder either way.","properties":{"kind":{"const":"file_ref","type":"string"},"path":{"type":"string"}},"required":["kind","path"],"type":"object"},{"additionalProperties":false,"description":"Explicitly selected, source-qualified Feature invocation. The host never\npromotes free text to this variant and validates it against the installed\ndeclaration before execution.","properties":{"invocation":{"$ref":"#/$defs/FeatureInvocation"},"kind":{"const":"feature_invoke","type":"string"}},"required":["kind","invocation"],"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,"submissions":[]}}},"required":["entries","pending_submissions"],"type":"object"},"SessionSnapshotEntry":{"oneOf":[{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"user_input","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"segments":{"items":{"$ref":"#/$defs/Segment"},"type":"array"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","segments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"items":{"$ref":"#/$defs/SessionContentPart"},"type":"array"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"message","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"role":{"$ref":"#/$defs/SessionMessageRole"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","role","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"arguments":{"type":"string"},"call_id":{"type":"string"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"tool_call","type":"string"},"name":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","name","arguments","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"attachments":{"items":{"$ref":"#/$defs/SessionToolAttachment"},"type":"array"},"call_id":{"type":"string"},"content":{"type":["string","null"]},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"is_error":{"type":"boolean"},"kind":{"const":"tool_result","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"summary":{"type":"string"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","call_id","summary","is_error","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"content":{"type":"string"},"data":true,"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"item_kind":{"type":"string"},"kind":{"const":"system_item","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","item_kind","content","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable, non-terminal control return from one logical Run. This is not a\ncompleted run and must not be used to infer the current Worker state.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_yielded","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"reason":{"$ref":"#/$defs/RunYieldReason"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","reason","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable boundary immediately before Engine execution resumes. The source\ndistinguishes automatic compaction continuation from an intentional\nuser-pause resume; it does not prove that later model output occurred.","properties":{"active_run_turn_count":{"format":"uint","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_resumed","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"source":{"$ref":"#/$defs/RunResumeSource"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","source","active_run_turn_count","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Snapshot-only replay of persisted invocation/usage records. These are\naccounting metadata, never model context or conversation rows.","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":"invoke","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"},"trigger":{"$ref":"#/$defs/InvokeKind"}},"required":["kind","trigger","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"cache_read_input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"input_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"kind":{"const":"usage","type":"string"},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"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","input_tokens","cache_read_input_tokens","output_tokens","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Display-only accounting reconstructed from persisted run/usage records.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"elapsed_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"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_stats","type":"string"},"output_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"requests":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"upload_tokens":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","elapsed_ms","requests","upload_tokens","output_tokens","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"description":"Durable terminal for an intentional cancellation. Consumers must not\npresent this lifecycle result as an execution failure.","properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"kind":{"const":"run_cancelled","type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","entry_id","timestamp","provenance"],"type":"object"},{"additionalProperties":false,"properties":{"derived_from":{"items":{"type":"string"},"type":"array"},"entry_id":{"description":"Stable identity from durable history metadata, or a deterministic\nidentity derived from the legacy segment and log position.","type":"string"},"failure":{"anyOf":[{"$ref":"#/$defs/RunFailureKind"},{"type":"null"}]},"kind":{"const":"run_error","type":"string"},"message":{"type":"string"},"provenance":{"$ref":"#/$defs/SessionEntryProvenance"},"timestamp":{"description":"Timestamp copied from the durable log record that commits this entry.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["kind","message","entry_id","timestamp","provenance"],"type":"object"}]},"SessionToolAttachment":{"additionalProperties":false,"properties":{"attachment_id":{"description":"Stable, bounded identity derived from the owning Session entry, attachment\nposition, and immutable image bytes. It is a locator, not a bearer secret.","type":"string"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"media_type":{"type":"string"}},"required":["attachment_id","media_type","byte_len"],"type":"object"},"SubmissionDisposition":{"description":"Durable acceptance result for one idempotent Submit request.","oneOf":[{"enum":["started"],"type":"string"},{"const":"queued","description":"Input is durably held in the session FIFO until eligible for activation.","type":"string"}]},"SubscriptionEvent":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"payload":{"$ref":"#/$defs/SubscriptionEventPayload"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","payload"],"type":"object"},"event":{"const":"event","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionTerminationCode"},"message":{"type":"string"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","code","message"],"type":"object"},"event":{"const":"subscription_closed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionEventPayload":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"worker":{"$ref":"#/$defs/SubscriptionWorker"}},"required":["worker"],"type":"object"},"event":{"const":"worker_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"runtime_id":{"type":["string","null"]},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id"],"type":"object"},"event":{"const":"worker_removed","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"event":{"$ref":"#/$defs/Event"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","event"],"type":"object"},"event":{"const":"worker_protocol","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdir":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"}},"required":["workdir"],"type":"object"},"event":{"const":"workdir_upserted","type":"string"}},"required":["event","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id"],"type":"object"},"event":{"const":"workdir_removed","type":"string"}},"required":["event","data"],"type":"object"}]},"SubscriptionId":{"type":"string"},"SubscriptionRejectionCode":{"enum":["invalid_request","unsupported_protocol_version","unsupported_selector","unauthorized","resource_not_found","capacity_exceeded","internal"],"type":"string"},"SubscriptionRequest":{"oneOf":[{"additionalProperties":false,"properties":{"method":{"const":"subscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"}},"required":["request_id","selector"],"type":"object"}},"required":["method","params"],"type":"object"},{"additionalProperties":false,"properties":{"method":{"const":"unsubscribe_events","type":"string"},"params":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"}},"required":["method","params"],"type":"object"}]},"SubscriptionRequestId":{"type":"string"},"SubscriptionResponse":{"oneOf":[{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"selector":{"$ref":"#/$defs/EventSubscriptionSelector"},"snapshot":{"$ref":"#/$defs/SubscriptionSnapshot"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id","selector","snapshot"],"type":"object"},"result":{"const":"subscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["request_id","subscription_id"],"type":"object"},"result":{"const":"unsubscribed","type":"string"}},"required":["result","payload"],"type":"object"},{"additionalProperties":false,"properties":{"payload":{"additionalProperties":false,"properties":{"code":{"$ref":"#/$defs/SubscriptionRejectionCode"},"message":{"type":"string"},"request_id":{"$ref":"#/$defs/SubscriptionRequestId"},"subscription_id":{"anyOf":[{"$ref":"#/$defs/SubscriptionId"},{"type":"null"}]}},"required":["request_id","code","message"],"type":"object"},"result":{"const":"subscription_rejected","type":"string"}},"required":["result","payload"],"type":"object"}]},"SubscriptionSnapshot":{"oneOf":[{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workers":{"items":{"$ref":"#/$defs/SubscriptionWorker"},"type":"array"}},"required":["workers"],"type":"object"},"topic":{"const":"workers","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"events":{"items":{"$ref":"#/$defs/Event"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"}},"required":["worker_id","events"],"type":"object"},"topic":{"const":"worker_protocol","type":"string"}},"required":["topic","data"],"type":"object"},{"additionalProperties":false,"properties":{"data":{"additionalProperties":false,"properties":{"workdirs":{"items":{"$ref":"#/$defs/WorkspaceSubscriptionWorkdir"},"type":"array"}},"required":["workdirs"],"type":"object"},"topic":{"const":"workspace_workdirs","type":"string"}},"required":["topic","data"],"type":"object"}]},"SubscriptionTerminationCode":{"enum":["lagged","resource_gone","unauthorized","server_shutdown"],"type":"string"},"SubscriptionWorkdirId":{"type":"string"},"SubscriptionWorker":{"additionalProperties":false,"properties":{"availability":{"$ref":"#/$defs/SubscriptionWorkerAvailability","default":"observed","description":"Freshness of the Runtime-backed observation carried by this projection.\n`Unavailable` preserves catalog membership without claiming that execution stopped."},"display_name":{"type":["string","null"]},"has_running_internal_workers":{"default":false,"type":"boolean"},"job":{"anyOf":[{"$ref":"#/$defs/SubscriptionWorkerJob"},{"type":"null"}],"description":"Backend-owned Job binding. Runtime producers leave this unset; the\nWorkspace projection enriches both snapshots and live updates."},"profile":{"type":["string","null"]},"resource_key":{"description":"Workspace-scoped canonical resource key. Runtime producers leave this unset;\nWorkspace-facing projections must populate it before publishing the Worker.","type":["string","null"]},"restore_observation_token":{"description":"Opaque Runtime-owned Restore observation identity, never a display-state hash.","type":["string","null"]},"runtime_id":{"description":"Set by the Workspace Server when projecting a Runtime-owned Worker to clients.\nRuntime producers leave this unset because the connection identifies the Runtime.","type":["string","null"]},"state":{"$ref":"#/$defs/SubscriptionWorkerState","description":"Runtime catalog lifecycle compatibility projection; not foreground-state authority."},"workdir_attachments":{"items":{"$ref":"#/$defs/SubscriptionWorkerWorkdirAttachment"},"type":"array"},"worker_id":{"$ref":"#/$defs/SubscriptionWorkerId"},"worker_state":{"anyOf":[{"$ref":"#/$defs/WorkerStateSnapshot"},{"type":"null"}],"description":"Latest foreground state observed from the Worker. This remains\nabsent until an authoritative Worker snapshot/event has been applied."},"workspace_id":{"type":["string","null"]}},"required":["worker_id","state","availability","has_running_internal_workers"],"type":"object"},"SubscriptionWorkerAvailability":{"description":"Whether the Workspace Server has a current Runtime observation for a Worker.\n\n`Unavailable` deliberately does not imply `Stopped`: it means the last observed\nlifecycle state is retained while the Runtime observation source is unavailable.","enum":["observed","unavailable"],"type":"string"},"SubscriptionWorkerId":{"type":"string"},"SubscriptionWorkerIds":{"description":"A bounded, canonical worker-id set. Ordering on the wire and in equality/hash\nsemantics is lexical, so equivalent selectors aggregate to one upstream key.","items":{"$ref":"#/$defs/SubscriptionWorkerId"},"type":"array"},"SubscriptionWorkerJob":{"additionalProperties":false,"properties":{"attempt_id":{"type":"string"},"job_id":{"type":"string"},"purpose":{"type":"string"}},"required":["job_id","attempt_id","purpose"],"type":"object"},"SubscriptionWorkerProtocolMethod":{"additionalProperties":false,"properties":{"method":{"$ref":"#/$defs/Method"},"subscription_id":{"$ref":"#/$defs/SubscriptionId"}},"required":["subscription_id","method"],"type":"object"},"SubscriptionWorkerState":{"enum":["idle","running","paused","stopped"],"type":"string"},"SubscriptionWorkerWorkdirAttachment":{"additionalProperties":false,"properties":{"alias":{"description":"Stable Worker-local routing alias.","type":"string"},"repository_key":{"type":["string","null"]},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["alias","working_directory_id"],"type":"object"},"SymlinkPolicy":{"description":"Symbolic-link identity used by one filesystem allow rule.\n\n`Resolved` is the least authority and the default: access is matched\nagainst the provider-resolved target. `Logical` intentionally grants the\npath as presented through the Workdir, even when it aliases another target.","enum":["resolved","logical"],"type":"string"},"ToolResultDisposition":{"enum":["success","error","interrupted","cancelled","outcome_unknown"],"type":"string"},"TurnResult":{"enum":["finished","paused"],"type":"string"},"UploadedFileAvailability":{"description":"Availability recorded for an uploaded client-local file.","enum":["available","unavailable","integrity_failed"],"type":"string"},"UploadedFileRef":{"additionalProperties":false,"description":"Session-owned immutable reference to a client-local uploaded file.\n\nUpload transports return an unbound reference. Worker fills\n`source_entry_id` immediately before the containing user input is committed;\ncommitted Session Log and public snapshot records therefore always retain\nthe durable source-entry identity without storing the file body.","properties":{"artifact_id":{"type":"string"},"availability":{"$ref":"#/$defs/UploadedFileAvailability"},"byte_len":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"created_at_ms":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"file_name":{"type":"string"},"media_type":{"type":"string"},"sha256":{"type":"string"},"source_entry_id":{"type":["string","null"]}},"required":["artifact_id","file_name","media_type","created_at_ms","availability","byte_len","sha256"],"type":"object"},"WorkerBusyState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"run","type":"string"},"state":{"$ref":"#/$defs/WorkerRunState"}},"required":["kind","state"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"maintenance","type":"string"},"state":{"$ref":"#/$defs/WorkerMaintenanceState"}},"required":["kind","state"],"type":"object"}]},"WorkerCommandAcknowledgement":{"additionalProperties":false,"properties":{"command":{"$ref":"#/$defs/WorkerCommandKind"},"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"disposition":{"$ref":"#/$defs/WorkerCommandDisposition"},"state":{"$ref":"#/$defs/WorkerStateSnapshot","description":"The complete authoritative state observed after command admission."}},"required":["command_id","command","disposition","state"],"type":"object"},"WorkerCommandDisposition":{"enum":["accepted","stale_command_id","conflict","invalid_state"],"type":"string"},"WorkerCommandEnvelope":{"additionalProperties":false,"description":"Caller-owned identity for one state-changing Worker command.\n\nCommands operate on the controller state at admission time, not a caller's\npreviously observed Run. A controller accepts command ids in strictly\nincreasing order; retrying an accepted id cannot apply it to a later Run.\nExact retries must retain the same command kind. Removed preconditions are\nrejected at the wire boundary rather than treated as arrival-time commands.","properties":{"command_id":{"format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"}},"required":["command_id"],"type":"object"},"WorkerCommandKind":{"enum":["resume","cancel","pause","compact","shutdown"],"type":"string"},"WorkerEvent":{"description":"Typed lifecycle events sent from a child Worker to its parent.\n\nDelivered as `Method::WorkerEvent` over the parent's Unix socket. The\nparent Controller always applies variant-specific side effects\n(registry / worker-allocation updates). Agent-visible variants are also\nqueued into the notification buffer; control-plane-only variants are\nnot injected into the parent's LLM context.\n\nTransport is fire-and-forget; receivers must tolerate out-of-order\ndelivery (e.g. `TurnEnded` arriving after `ShutDown` for the same\nchild Worker).","oneOf":[{"additionalProperties":false,"description":"Child finished one turn and is back to IDLE.","properties":{"kind":{"const":"turn_ended","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Engine execution error occurred inside the child's turn.\n\nLimited to worker runtime failures (provider / tool errors) —\ndoes not include transient method-rejection responses such as\n`AlreadyRunning`.","properties":{"kind":{"const":"errored","type":"string"},"message":{"type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name","message"],"type":"object"},{"additionalProperties":false,"description":"Child has stopped (controller loop is exiting).","properties":{"kind":{"const":"shut_down","type":"string"},"worker_name":{"type":"string"}},"required":["kind","worker_name"],"type":"object"},{"additionalProperties":false,"description":"Child SubWorker sub-delegated scope to a grandchild SubWorker via `SubWorkerSpawn`.\n\nControl-plane only: receivers apply registry side effects and\npropagate upward, but do not expose this as an agent notification.\n\nThe parent uses this to add the grandchild to its own\n`spawned_workers.json` so it can manage the grandchild directly\neven if the intermediate child dies. The parent then re-fires\nthis event upward (if it has a parent of its own) to maintain\nthe chain to root.","properties":{"kind":{"const":"scope_sub_delegated","type":"string"},"parent_worker":{"description":"Sub-delegating Worker (= the sender itself).","type":"string"},"scope":{"description":"Scope delegated to the grandchild.","items":{"$ref":"#/$defs/ScopeRule"},"type":"array"},"sub_socket":{"description":"Unix-socket path where the grandchild is reachable.","type":"string"},"sub_worker":{"description":"Name of the grandchild Worker.","type":"string"}},"required":["kind","parent_worker","sub_worker","sub_socket","scope"],"type":"object"}]},"WorkerMaintenanceState":{"enum":["compacting"],"type":"string"},"WorkerRunState":{"enum":["running","pausing","paused","cancelling"],"type":"string"},"WorkerState":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"idle","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"busy","type":"string"},"state":{"$ref":"#/$defs/WorkerBusyState"}},"required":["kind","state"],"type":"object"}]},"WorkerStateSnapshot":{"additionalProperties":false,"properties":{"last_command_id":{"description":"Highest lifecycle command id observed by this controller instance.","format":"uint64","maximum":9007199254740991.0,"minimum":0.0,"type":"integer"},"last_finished_submission_request_id":{"description":"Most recent Submit request whose controller Run has returned and whose\npost-Run state transition has been published. This is a teardown fence,\nnot a successful-result claim; cancelled and failed Runs may also finish.","type":["string","null"]},"state":{"$ref":"#/$defs/WorkerState"}},"required":["last_command_id","state"],"type":"object"},"WorkerStatus":{"enum":["idle","running","paused","stopped"],"type":"string"},"WorkspaceSubscriptionWorkdir":{"additionalProperties":false,"description":"Workspace-facing Workdir summary. Backend-generated Repository UUIDs never\nenter this DTO; Workspace Server must resolve the required Repository key.","properties":{"repository_key":{"type":"string"},"state":{"type":"string"},"working_directory_id":{"$ref":"#/$defs/SubscriptionWorkdirId"}},"required":["working_directory_id","repository_key","state"],"type":"object"}},"$schema":"https://json-schema.org/draft/2020-12/schema","description":"Direction is explicit at the outer envelope, while concrete message kinds\nremain typed within each lane.","oneOf":[{"additionalProperties":false,"properties":{"frame":{"const":"request","type":"string"},"message":{"$ref":"#/$defs/SubscriptionRequest"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"response","type":"string"},"message":{"$ref":"#/$defs/SubscriptionResponse"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"event","type":"string"},"message":{"$ref":"#/$defs/SubscriptionEvent"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"},{"additionalProperties":false,"properties":{"frame":{"const":"worker_protocol","type":"string"},"message":{"$ref":"#/$defs/SubscriptionWorkerProtocolMethod"},"protocol_version":{"format":"uint16","maximum":65535.0,"minimum":0.0,"type":"integer"}},"required":["frame","message","protocol_version"],"type":"object"}],"title":"SubscriptionFrame"}; export const RUST_SERIALIZED_SUBSCRIPTION_FRAME_FIXTURES: readonly unknown[] = [{"protocol_version":2,"frame":"response","message":{"result":"subscribed","payload":{"request_id":"fixture-request","subscription_id":"fixture-subscription","selector":{"topic":"workspace_workers"},"snapshot":{"topic":"workers","data":{"workers":[{"restore_observation_token":"fixture-restore-observation","worker_id":"fixture-worker","runtime_id":"fixture-runtime","resource_key":"fixture-worker-resource","availability":"observed","state":"idle","has_running_internal_workers":false,"workspace_id":"fixture-workspace","workdir_attachments":[{"alias":"external","working_directory_id":"fixture-external-workdir"}]}]}}}}},{"protocol_version":2,"frame":"event","message":{"event":"event","data":{"subscription_id":"fixture-subscription","payload":{"event":"worker_protocol","data":{"worker_id":"fixture-worker","event":{"event":"pending_submissions_changed","data":{"pending":{"notification_count":0,"head_id":null,"submissions":[]}}}}}}}},{"protocol_version":2,"frame":"event","message":{"event":"event","data":{"subscription_id":"fixture-subscription","payload":{"event":"worker_protocol","data":{"worker_id":"fixture-worker","event":{"event":"snapshot","data":{"session":{"pending_submissions":{"notification_count":0,"head_id":null,"submissions":[]},"entries":[]},"greeting":{"worker_name":"fixture-worker","cwd":"/fixture","provider":"openai_responses","model":"gpt-6-astra","reasoning":{"kind":"effort","effort":"high"},"scope_summary":"fixture scope","tools":["Read"],"context_window":272000,"context_tokens":142000,"context_usage":{"tokens":142000,"source":"measured"}},"state":{"last_command_id":0,"state":{"kind":"idle"}},"internal_workers":[{"worker":{"session_id":"fixture-child-session","name":"fixture-child","parent_session_id":"fixture-parent-session","kind":"sub_worker"},"session":{"pending_submissions":{"notification_count":0,"head_id":null,"submissions":[]},"entries":[]},"greeting":{"worker_name":"fixture-worker","cwd":"/fixture","provider":"openai_responses","model":"gpt-6-astra","reasoning":{"kind":"effort","effort":"high"},"scope_summary":"fixture scope","tools":["Read"],"context_window":272000,"context_tokens":142000,"context_usage":{"tokens":142000,"source":"measured"}},"status":"idle"}]}}}}}}}]; const utf8 = new TextEncoder(); diff --git a/web/workspace/src/lib/generated/protocol.ts b/web/workspace/src/lib/generated/protocol.ts index 6aca4e1ae..8b6870f39 100644 --- a/web/workspace/src/lib/generated/protocol.ts +++ b/web/workspace/src/lib/generated/protocol.ts @@ -4,6 +4,8 @@ export type AlertLevel = "warn" | "error"; +export type AlertPosition = { "kind": "start" } | { "kind": "after_entry", entry_id: string, } | { "kind": "after_tool_call", call_id: string, } | { "kind": "after_in_flight", block_id: number, }; + export type AlertSource = "worker" | "engine" | "compactor" | "agents_md"; export type CompletionKind = "file" | "feature" | "feature_argument"; @@ -161,7 +163,11 @@ export type InFlightBlock = { "kind": "text", text: string, finished?: boolean, export type InFlightCompaction = { phase: CompactionPhase, started_at_ms: number, trigger: CompactionTrigger, }; -export type InFlightSnapshot = { blocks?: Array, commands?: Array, +export type InFlightSnapshot = { +/** + * Live-Worker identities parallel to the blocks actually included below. + */ +block_ids?: Array, blocks?: Array, commands?: Array, /** * The currently running compaction, if any. * @@ -284,7 +290,11 @@ export type InternalWorkerSnapshot = { worker: InternalWorkerRef, session: Sessi */ greeting?: Greeting | null, status: WorkerStatus, error?: string | null, in_flight?: InFlightSnapshot, internal_workers?: Array, }; -export type Alert = { level: AlertLevel, source: AlertSource, message: string, +export type Alert = { +/** + * Stable only within the emitting live Worker; not durable alert history. + */ +id?: string | null, position?: AlertPosition | null, level: AlertLevel, source: AlertSource, message: string, /** * Milliseconds since the Unix epoch. */ diff --git a/web/workspace/src/lib/workspace/console/ConsoleLineItem.browser.spec.ts b/web/workspace/src/lib/workspace/console/ConsoleLineItem.browser.spec.ts index 03d5230a1..f00d45865 100644 --- a/web/workspace/src/lib/workspace/console/ConsoleLineItem.browser.spec.ts +++ b/web/workspace/src/lib/workspace/console/ConsoleLineItem.browser.spec.ts @@ -1,13 +1,101 @@ // @vitest-environment happy-dom -import type { Event } from "#lib/generated/protocol.ts"; +import type { AlertLevel, Event } from "#lib/generated/protocol.ts"; import { cleanup, render, waitFor } from "@testing-library/svelte"; import { afterEach, expect, test } from "vitest"; +import { Window } from "happy-dom"; +import { compile } from "svelte/compiler"; import ConsoleLineItem from "./ConsoleLineItem.svelte"; -import { createConsoleProjector } from "./model.ts"; +import componentSource from "./ConsoleLineItem.svelte?raw"; +import { type ConsoleLine, createConsoleProjector } from "./model.ts"; afterEach(cleanup); +// Vitest does not attach extracted component CSS. Compile the real scoped CSS +// into an isolated style document so the assertions exercise the cascade. +function styledLine(container: HTMLElement, colorScheme: "light" | "dark") { + const scope = [...container.querySelector("li")!.classList].find((name) => + name.startsWith("svelte-") + )!; + const componentCss = compile(componentSource, { + filename: "ConsoleLineItem.svelte", + cssHash: () => scope, + }).css!.code; + const browser = new Window({ + settings: { device: { prefersColorScheme: colorScheme } }, + }); + const style = browser.document.createElement("style"); + // Happy DOM cannot resolve app.css's @layer/oklch tokens. These sentinel + // values verify that non-Alerts still use the unchanged semantic tokens. + style.textContent = + `:root { --danger: #cc3344; --text-muted: #777777; }\n${componentCss}`; + browser.document.head.append(style); + browser.document.body.innerHTML = container.innerHTML; + return { + heading: browser.getComputedStyle( + browser.document.querySelector(".message-heading")!, + ).color, + body: browser.getComputedStyle( + browser.document.querySelector(".console-plain-text")!, + ).color, + }; +} + +for (const colorScheme of ["light", "dark"] as const) { + for (const level of ["warn", "error"] satisfies AlertLevel[]) { + test(`${level} Alert has an orange heading and readable grey body in ${colorScheme}`, () => { + const item: ConsoleLine = { + id: `alert-${level}`, + kind: level === "error" ? "error" : "status", + title: "Alert · engine", + body: "The engine needs attention.", + source: "event", + error: level === "error", + alert: { + level, + source: "engine", + message: "The engine needs attention.", + timestamp_ms: 1, + }, + }; + const { container } = render(ConsoleLineItem, { item }); + expect(container.querySelector("li")?.classList.contains("alert-line")) + .toBe(true); + expect(container.querySelector(".message-heading")?.textContent).toBe( + item.title, + ); + expect(container.querySelector(".console-plain-text")?.textContent).toBe( + item.body, + ); + expect(styledLine(container, colorScheme)).toEqual({ + heading: colorScheme === "light" ? "#a74709" : "#f6a75c", + body: colorScheme === "light" ? "#595959" : "#b8b8b8", + }); + }); + } + + test(`non-Alert errors retain error styling in ${colorScheme}, even with an Alert-like title`, () => { + const { container } = render(ConsoleLineItem, { + item: { + id: "ordinary-error", + kind: "error", + title: "Alert · engine", + body: "An ordinary error, not a protocol Alert.", + source: "event", + error: true, + }, + }); + expect(container.querySelector("li")?.classList.contains("alert-line")) + .toBe(false); + expect(container.querySelector("li")?.classList.contains("error-line")) + .toBe(true); + expect(styledLine(container, colorScheme)).toEqual({ + heading: "#777777", + body: "#cc3344", + }); + }); +} + function reconnectSnapshot(text: string): Event { return { event: "snapshot", @@ -15,7 +103,6 @@ function reconnectSnapshot(text: string): Event { session: { entries: [], pending_submissions: { - head_id: null, notification_count: 0, submissions: [], diff --git a/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte b/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte index bcc59fd3c..c861e74fc 100644 --- a/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte +++ b/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte @@ -85,6 +85,7 @@
  • void): void }; + +function assertEquals(actual: unknown, expected: unknown): void { + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error( + `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + ); + } +} + +let sequence = 0; +function apply( + projection: ConsoleProjection, + event: Event, + observedAtMs = 10000, +): ConsoleProjection { + return applyProtocolEvent(projection, { + eventId: `event-${sequence++}`, + event, + observedAtMs, + }); +} + +function message( + id: string, + timestamp: number, + body = id, +): SessionSnapshotEntry { + return { + kind: "message", + entry_id: id, + timestamp, + provenance: "model_output", + derived_from: [], + role: "assistant", + content: [{ kind: "text", text: body }], + }; +} + +function readCall(id: string, timestamp: number): SessionSnapshotEntry { + return { + kind: "tool_call", + entry_id: `entry-${id}`, + timestamp, + provenance: "model_output", + derived_from: [], + call_id: id, + name: "Read", + arguments: JSON.stringify({ file_path: `${id}.ts` }), + }; +} + +function snapshot( + entries: SessionSnapshotEntry[], + inFlight?: InFlightSnapshot, +): Event { + return { + event: "snapshot", + data: { + greeting: { + worker_name: "Worker", + cwd: "/workspace", + provider: "provider", + model: "model", + scope_summary: "bounded", + tools: [], + context_window: 100, + context_tokens: 20, + }, + state: { last_command_id: 0, state: { kind: "idle" } }, + session: { + entries, + pending_submissions: { + notification_count: 0, + head_id: null, + submissions: [], + }, + }, + in_flight: inFlight, + internal_workers: [], + }, + }; +} + +function alert( + id: string | undefined, + position?: AlertPosition, + timestamp = 150, +): Alert { + return { + id, + position, + level: "warn", + source: "worker", + message: "same warning", + timestamp_ms: timestamp, + }; +} + +function emit(projection: ConsoleProjection, data: Alert): ConsoleProjection { + return apply(projection, { event: "alert", data }); +} + +function commit( + projection: ConsoleProjection, + entry: SessionSnapshotEntry, +): ConsoleProjection { + return apply(projection, { + event: "session_entry_committed", + data: { entry }, + }); +} + +function labels(lines: ConsoleLine[]): string[] { + return lines.map((line) => + line.alert + ? `alert:${line.alert.id ?? "legacy"}` + : line.toolCall?.id ?? line.body + ); +} + +function visible(projection: ConsoleProjection): string[] { + return labels(projectVisibleConsole(projection).lines); +} + +Deno.test("live Alert chronology equals fresh snapshot plus replay without timestamp sorting", () => { + const entries = [ + message("first", 300), + message("second", 100), + message("third", 200), + ]; + const warnings = [ + alert("start", { kind: "start" }), + alert("a", { kind: "after_entry", entry_id: "first" }), + alert("b", { kind: "after_entry", entry_id: "first" }), + alert("c", { kind: "after_entry", entry_id: "second" }), + ]; + let live = emit(emptyConsoleProjection(), warnings[0]); + live = commit(live, entries[0]); + live = emit(emit(live, warnings[1]), warnings[2]); + live = emit(commit(live, entries[1]), warnings[3]); + live = commit(live, entries[2]); + let replay = apply(emptyConsoleProjection(), snapshot(entries)); + for (const warning of warnings) replay = emit(replay, warning); + assertEquals(visible(live), [ + "alert:start", + "first", + "alert:a", + "alert:b", + "second", + "alert:c", + "third", + ]); + assertEquals(visible(replay), visible(live)); +}); + +Deno.test("same-id replay updates metadata; distinct same-ms same-body IDs remain distinct", () => { + const entries = [message("one", 100), message("two", 200)]; + let projection = apply(emptyConsoleProjection(), snapshot(entries)); + projection = emit( + projection, + alert("a", { kind: "after_entry", entry_id: "one" }), + ); + projection = emit( + projection, + alert("b", { kind: "after_entry", entry_id: "one" }), + ); + const originalId = + projection.lines.find((line) => line.alert?.id === "a")!.id; + projection = apply(projection, snapshot(entries)); + assertEquals(visible(projection), ["one", "alert:a", "alert:b", "two"]); + projection = emit( + projection, + alert("a", { kind: "after_entry", entry_id: "two" }), + ); + projection = emit( + projection, + alert("a", { kind: "after_entry", entry_id: "two" }), + ); + projection = emit( + projection, + alert("b", { kind: "after_entry", entry_id: "one" }), + ); + assertEquals(visible(projection), ["one", "alert:b", "two", "alert:a"]); + assertEquals( + projection.lines.find((line) => line.alert?.id === "a")!.id, + originalId, + ); + assertEquals(projection.lines.filter((line) => line.alert).length, 2); +}); + +Deno.test("live pending Alert anchor follows the actual assistant through commit and reconnect", () => { + let live = commit(emptyConsoleProjection(), message("old", 10)); + live = apply(live, { event: "text_delta", data: { text: "pending" } }, 100); + live = emit( + live, + alert("pending-alert", { kind: "after_in_flight", block_id: 42 }), + ); + assertEquals(visible(live), ["old", "pending", "alert:pending-alert"]); + assertEquals( + live.lines.find((line) => line.body === "pending")?.inFlightBlockId, + 42, + ); + live = apply( + live, + { event: "text_done", data: { text: "pending answer" } }, + 180, + ); + live = commit(live, message("answer", 190, "pending answer")); + live = commit(live, message("later", 200)); + assertEquals(visible(live), [ + "old", + "pending answer", + "alert:pending-alert", + "later", + ]); + assertEquals(live.lines.find((line) => line.alert)?.alert?.position, { + kind: "after_entry", + entry_id: "answer", + }); + assertEquals( + live.lines.find((line) => line.entryId === "answer")?.inFlightBlockId, + 42, + ); + // Even a repeated pending-anchor payload cannot bind to a later assistant. + live = emit( + live, + alert("pending-alert", { kind: "after_in_flight", block_id: 42 }), + ); + assertEquals(visible(live), [ + "old", + "pending answer", + "alert:pending-alert", + "later", + ]); + const entries = [ + message("old", 10), + message("answer", 190, "pending answer"), + message("later", 200), + ]; + live = apply(live, snapshot(entries)); + assertEquals(visible(live), [ + "old", + "pending answer", + "alert:pending-alert", + "later", + ]); + const resolved = alert("pending-alert", { + kind: "after_entry", + entry_id: "answer", + }); + live = emit(live, resolved); + const fresh = emit( + apply(emptyConsoleProjection(), snapshot(entries)), + resolved, + ); + assertEquals(visible(live), visible(fresh)); +}); + +Deno.test("snapshot block IDs restore pending anchors and survive text continuation", () => { + const entries = [message("old", 10)]; + const inFlight: InFlightSnapshot = { + block_ids: [7, 8, 9], + blocks: [ + { kind: "text", text: "first pending", finished: true }, + { + kind: "tool_call", + id: "read", + name: "Read", + args: "{}", + state: "done", + }, + { kind: "text", text: "second", finished: false }, + ], + }; + let projection = apply(emptyConsoleProjection(), snapshot(entries, inFlight)); + projection = emit( + projection, + alert("a", { kind: "after_in_flight", block_id: 7 }), + ); + projection = emit( + projection, + alert("b", { kind: "after_in_flight", block_id: 9 }), + ); + const before = visible(projection); + projection = apply(projection, snapshot(entries, inFlight)); + assertEquals(visible(projection), before); + assertEquals(before, [ + "old", + "first pending", + "alert:a", + "read", + "second", + "alert:b", + ]); + projection = commit(projection, message("first", 100, "first pending")); + projection = apply(projection, { + event: "text_delta", + data: { text: " pending" }, + }); + projection = apply(projection, { + event: "text_done", + data: { text: "second pending" }, + }); + projection = commit(projection, message("second", 200, "second pending")); + assertEquals(visible(projection), [ + "old", + "first pending", + "alert:a", + "read", + "second pending", + "alert:b", + ]); + assertEquals( + projection.lines.filter((line) => line.alert).map((line) => + line.alert!.position + ), + [ + { kind: "after_entry", entry_id: "first" }, + { kind: "after_entry", entry_id: "second" }, + ], + ); +}); + +Deno.test("snapshot with committed pending text retains Alerts until replay resolves metadata", () => { + let projection = apply(emptyConsoleProjection(), { + event: "text_delta", + data: { text: "answer" }, + }); + projection = emit( + projection, + alert("a", { kind: "after_in_flight", block_id: 15 }), + ); + projection = apply( + projection, + snapshot([message("answer", 100), message("later", 200)]), + ); + assertEquals(projection.lines.filter((line) => line.alert).length, 1); + projection = emit( + projection, + alert("a", { kind: "after_entry", entry_id: "answer" }), + ); + assertEquals(visible(projection), ["answer", "alert:a", "later"]); +}); + +Deno.test("Alert anchors split Read and overview aggregation before and after history merge", () => { + const entries = [ + readCall("r1", 100), + readCall("r2", 200), + readCall("r3", 300), + ]; + let projection = apply(emptyConsoleProjection(), snapshot(entries)); + projection = emit( + projection, + alert("boundary", { kind: "after_tool_call", call_id: "r1" }), + ); + const lines = projectVisibleConsole(projection).lines; + assertEquals( + lines.map((line) => line.alert ? "alert" : line.toolCallCount ?? 1), + [1, "alert", 2], + ); + assertEquals( + projectConsoleLines(lines, "overview").map((line) => line.kind), + ["activity", "status", "activity"], + ); + const history = projectSessionHistoryEntries(entries, null); + const merged = mergeCommittedHistoryLines(history, lines); + assertEquals( + merged.map((line) => line.alert ? "alert" : line.toolCallCount ?? 1), + [1, "alert", 2], + ); + assertEquals( + projectConsoleLines(merged, "overview").map((line) => line.kind), + ["activity", "status", "activity"], + ); +}); + +Deno.test("missing older anchors stay before the window and resolve when history arrives", () => { + let projection = apply( + emptyConsoleProjection(), + snapshot([message("current", 300)]), + ); + projection = emit( + projection, + alert("older", { kind: "after_entry", entry_id: "older" }), + ); + projection = emit( + projection, + alert("same-anchor", { kind: "after_entry", entry_id: "older" }), + ); + projection = emit(projection, alert("start", { kind: "start" })); + const current = projectVisibleConsole(projection).lines; + assertEquals(labels(current), [ + "alert:older", + "alert:same-anchor", + "alert:start", + "current", + ]); + const history = projectSessionHistoryEntries([ + message("first", 50), + message("older", 100), + message("middle", 200), + message("current", 300), + ], null); + const merged = mergeCommittedHistoryLines(history, current); + assertEquals(labels(merged), [ + "alert:start", + "first", + "older", + "alert:older", + "alert:same-anchor", + "middle", + "current", + ]); + assertEquals( + labels(mergeCommittedHistoryLines(history, merged)), + labels(merged), + ); +}); + +Deno.test("legacy Alerts use original emission timestamps for live and replay chronology", () => { + let live = apply(emptyConsoleProjection(), { + event: "text_done", + data: { text: "first" }, + }, 100); + live = emit(live, alert(undefined, undefined, 150)); + live = apply(live, { event: "text_done", data: { text: "second" } }, 200); + assertEquals(visible(live), ["first", "alert:legacy", "second"]); + let replay = apply( + emptyConsoleProjection(), + snapshot([message("first", 100), message("second", 200)]), + ); + replay = emit(replay, alert(undefined, undefined, 150)); + replay = emit(replay, alert(undefined, undefined, 150)); + assertEquals(visible(replay), visible(live)); + assertEquals(replay.lines.map((line) => line.timestampMs), [100, 200, 150]); + const merged = mergeCommittedHistoryLines( + projectSessionHistoryEntries([message("earliest", 50)], null), + projectVisibleConsole(replay).lines, + ); + assertEquals(labels(merged), ["earliest", "first", "alert:legacy", "second"]); +}); + +Deno.test("appended entry metadata does not rescan prior event IDs or mutate retained rows", () => { + const projection = apply( + emptyConsoleProjection(), + snapshot( + Array.from( + { length: 128 }, + (_, index) => message(`entry-${index}`, index), + ), + ), + ); + let priorEventIdReads = 0; + projection.lines = projection.lines.map((line) => + Object.freeze({ + ...line, + get eventId() { + priorEventIdReads += 1; + return line.eventId; + }, + }) + ); + const next = commit(projection, message("appended", 200)); + assertEquals(priorEventIdReads, 0); + assertEquals(next.lines.length, projection.lines.length + 1); + assertEquals( + next.lines.slice(0, -1).every((line, index) => + line === projection.lines[index] + ), + true, + ); + assertEquals(next.lines.at(-1)?.entryId, "appended"); + assertEquals(next.lines.at(-1)?.sourceEntryIds, ["appended"]); + assertEquals(next.lines.at(-1)?.timestampMs, 200); +}); + +Deno.test("snapshot retention indexes previous entry metadata in one pass", () => { + const entries = Array.from( + { length: 128 }, + (_, index) => message(`entry-${index}`, index), + ); + const projection = apply(emptyConsoleProjection(), snapshot(entries)); + let priorEntryIdReads = 0; + projection.lines = projection.lines.map((line, index) => + Object.freeze({ + ...line, + inFlightBlockId: index, + get entryId() { + priorEntryIdReads += 1; + return line.entryId; + }, + }) + ); + const next = apply(projection, snapshot(entries)); + assertEquals(priorEntryIdReads, entries.length); + assertEquals( + next.lines.map((line) => line.inFlightBlockId), + entries.map((_, index) => index), + ); + assertEquals( + next.lines.map((line) => line.timestampMs), + entries.map((_, index) => index), + ); +}); + +Deno.test("tool-result reconciliation retains call-entry anchors and creation timestamp", () => { + let projection = commit(emptyConsoleProjection(), readCall("r1", 100)); + projection = emit( + projection, + alert("a", { kind: "after_entry", entry_id: "entry-r1" }), + ); + projection = commit(projection, readCall("r2", 200)); + const before = projection; + const originalCall = before.lines[0]; + Object.freeze(originalCall.sourceEntryIds); + Object.freeze(originalCall); + projection = commit(projection, { + kind: "tool_result", + entry_id: "result-r1", + timestamp: 250, + provenance: "tool_output", + derived_from: [], + call_id: "r1", + summary: "read", + content: "text", + is_error: false, + }); + assertEquals(visible(projection), ["r1", "alert:a", "r2"]); + assertEquals(originalCall.sourceEntryIds, ["entry-r1"]); + assertEquals(originalCall.entryId, "entry-r1"); + assertEquals(originalCall.toolCall?.state, "running"); + assertEquals(projection.lines[0] === originalCall, false); + assertEquals(projection.lines[2] === before.lines[2], true); + assertEquals(projection.lines[0].sourceEntryIds, ["entry-r1", "result-r1"]); + assertEquals( + projection.lines.find((line) => line.toolCall?.id === "r1")?.timestampMs, + 100, + ); + projection = emit( + projection, + alert("a", { kind: "after_entry", entry_id: "result-r1" }), + ); + assertEquals(visible(projection), ["r1", "alert:a", "r2"]); +}); + +Deno.test("history enriches a live tool row with durable anchors before placing Alerts", () => { + let projection = apply(emptyConsoleProjection(), { + event: "tool_call_start", + data: { id: "r1", name: "Read" }, + }, 110); + projection = emit( + projection, + alert("a", { kind: "after_entry", entry_id: "entry-r1" }), + ); + projection = apply(projection, { + event: "tool_call_start", + data: { id: "r2", name: "Read" }, + }, 210); + const merged = mergeCommittedHistoryLines( + projectSessionHistoryEntries( + [readCall("r1", 100), readCall("r2", 200)], + null, + ), + projectVisibleConsole(projection).lines, + ); + assertEquals(labels(merged), ["r1", "alert:a", "r2"]); + assertEquals(merged[0].toolCallLines?.[0].timestampMs, 100); + assertEquals(merged[0].toolCallLines?.[0].toolCall?.state, "pending"); +}); + +Deno.test("live pending metadata survives reconnect before commit without changing first observation", () => { + let projection = apply(emptyConsoleProjection(), { + event: "text_delta", + data: { text: "pending" }, + }, 100); + const warning = alert("a", { kind: "after_in_flight", block_id: 27 }); + projection = emit(projection, warning); + projection = apply( + projection, + snapshot([], { + block_ids: [27], + blocks: [{ kind: "text", text: "pending", finished: false }], + }), + 5000, + ); + assertEquals(visible(projection), ["pending", "alert:a"]); + assertEquals(projection.lines[0].timestampMs, 100); + projection = emit(projection, warning); + assertEquals(visible(projection), ["pending", "alert:a"]); + projection = apply(projection, { + event: "text_done", + data: { text: "pending" }, + }); + projection = commit(projection, message("answer", 200, "pending")); + assertEquals(projection.lines.find((line) => line.alert)?.alert?.position, { + kind: "after_entry", + entry_id: "answer", + }); +}); + +Deno.test("parent snapshot retains child Alerts in their own console only", () => { + const worker = { + kind: "sub_worker", + name: "child", + session_id: "child-session", + } as const; + const childEvent = (event: Event): Event => ({ + event: "internal_worker", + data: { worker, event }, + }); + let projection = apply( + emptyConsoleProjection(), + childEvent(snapshot([message("child-answer", 100)])), + ); + projection = apply( + projection, + childEvent({ + event: "alert", + data: alert("a", { kind: "after_entry", entry_id: "child-answer" }), + }), + ); + const parentSnapshot = snapshot([message("parent", 300)]); + if (parentSnapshot.event !== "snapshot") throw new Error("snapshot expected"); + const childSnapshot = snapshot([message("child-answer", 100)]); + if (childSnapshot.event !== "snapshot") throw new Error("snapshot expected"); + parentSnapshot.data.internal_workers = [{ + worker, + status: "idle", + error: null, + session: childSnapshot.data.session, + }]; + projection = apply(projection, parentSnapshot); + assertEquals(visible(projection), ["parent"]); + assertEquals(visible(projection.internalWorkers[0].console), [ + "child-answer", + "alert:a", + ]); +}); + +Deno.test("snapshot retains warning Alerts but reconciles durable run errors", () => { + let projection = emit( + emptyConsoleProjection(), + alert("warn", { kind: "start" }), + ); + projection = emit(projection, { + ...alert("error", { kind: "start" }), + level: "error", + message: "failed", + }); + projection = apply( + projection, + snapshot([{ + kind: "run_error", + entry_id: "failure", + timestamp: 300, + provenance: "legacy_unknown", + derived_from: [], + message: "failed", + failure: "compaction", + }]), + ); + assertEquals( + projectVisibleConsole(projection).lines.map((line) => + line.alert?.id ?? line.title + ), + ["warn", "Compaction failed"], + ); +}); diff --git a/web/workspace/src/lib/workspace/console/alert-order.ts b/web/workspace/src/lib/workspace/console/alert-order.ts new file mode 100644 index 000000000..81c44c551 --- /dev/null +++ b/web/workspace/src/lib/workspace/console/alert-order.ts @@ -0,0 +1,131 @@ +import type { Alert } from "#lib/generated/protocol.ts"; +import type { ConsoleLine } from "./model.ts"; + +/** Stable IDs are authoritative; legacy replay can only use the original payload. */ +export function alertIdentity(alert: Alert): string { + return alert.id != null ? `id:${alert.id}` : `legacy:${ + JSON.stringify([ + alert.timestamp_ms, + alert.level, + alert.source, + alert.message, + ]) + }`; +} + +/** Keep first-seen order while allowing replay to refresh anchor metadata. */ +export function deduplicateAlertLines(lines: ConsoleLine[]): ConsoleLine[] { + const result: ConsoleLine[] = []; + const positions = new Map(); + for (const line of lines) { + if (!line.alert) { + result.push(line); + continue; + } + const key = alertIdentity(line.alert); + const previous = positions.get(key); + if (previous === undefined) { + positions.set(key, result.length); + result.push(line); + } else { + result[previous] = { ...line, id: result[previous]!.id }; + } + } + return result; +} + +/** + * Insert Alerts between source rows BEFORE any Read/activity aggregation. + * Conversation order remains authoritative, even when timestamps are skewed. + * An absent explicit anchor belongs before the loaded window, not at its tail; + * its metadata remains intact so loading older history can resolve it later. + */ +export function orderAlertLines(lines: ConsoleLine[]): ConsoleLine[] { + const ungrouped = lines.flatMap((line) => line.toolCallLines ?? [line]); + const unique = deduplicateAlertLines(ungrouped); + const source = unique.filter((line) => !line.alert); + const slots = new Map(); + for (const line of unique) { + const alert = line.alert; + if (!alert) continue; + const position = alert.position; + let index: number; + switch (position?.kind) { + case "start": + index = 0; + break; + case "after_entry": + index = source.findIndex((row) => + row.entryId === position.entry_id || + row.sourceEntryIds?.includes(position.entry_id) + ) + 1; + break; + case "after_tool_call": + index = source.findIndex((row) => + row.toolCall?.id === position.call_id + ) + 1; + break; + case "after_in_flight": + index = + source.findIndex((row) => row.inFlightBlockId === position.block_id) + + 1; + break; + default: { + // Legacy senders have no identity/anchor. Use emission time, never the + // reconnect observation time, without sorting the conversation itself. + const later = source.findIndex((row) => + row.timestampMs !== undefined && row.timestampMs > alert.timestamp_ms + ); + index = later < 0 ? source.length : later; + } + } + const slot = slots.get(index) ?? []; + slot.push(line); + slots.set(index, slot); + } + const result = [...(slots.get(0) ?? [])]; + source.forEach((line, index) => { + result.push(line, ...(slots.get(index + 1) ?? [])); + }); + return result; +} + +/** Live deltas lack block IDs; the emitting Alert identifies the pending row. */ +export function attachAlertInFlightAnchor( + lines: ConsoleLine[], + alert: Alert, +): ConsoleLine[] { + const position = alert.position; + if (position?.kind !== "after_in_flight") return lines; + if (lines.some((line) => line.inFlightBlockId === position.block_id)) { + return lines; + } + const index = lines.findLastIndex((line) => + line.kind === "assistant" && !line.entryId + ); + if (index < 0 || lines[index].inFlightBlockId !== undefined) return lines; + return lines.map((line, rowIndex) => + rowIndex === index ? { ...line, inFlightBlockId: position.block_id } : line + ); +} + +/** Resolve before the pending row is removed; replay may repeat either anchor. */ +export function resolveCommittedAlertAnchors( + lines: ConsoleLine[], + blockId: number | undefined, + entryId: string, +): ConsoleLine[] { + if (blockId === undefined) return lines; + return lines.map((line) => + line.alert?.position?.kind === "after_in_flight" && + line.alert.position.block_id === blockId + ? { + ...line, + alert: { + ...line.alert, + position: { kind: "after_entry", entry_id: entryId }, + }, + } + : line + ); +} diff --git a/web/workspace/src/lib/workspace/console/model.ts b/web/workspace/src/lib/workspace/console/model.ts index 455913e53..195d14bec 100644 --- a/web/workspace/src/lib/workspace/console/model.ts +++ b/web/workspace/src/lib/workspace/console/model.ts @@ -24,6 +24,12 @@ import type { WorkerStatus, } from "#lib/generated/protocol.ts"; import { invocationInput } from "./composer-invocation.ts"; +import { + attachAlertInFlightAnchor, + deduplicateAlertLines, + orderAlertLines, + resolveCommittedAlertAnchors, +} from "./alert-order.ts"; import { stringify as stringifyYaml } from "yaml"; import { applyRunActivityEvent, @@ -103,6 +109,12 @@ export type ConsoleLine = { eventId?: string | null; /** Stable durable identity when this line originates from a committed entry. */ entryId?: string; + /** Calls/results can share a row while retaining distinct durable anchors. */ + sourceEntryIds?: string[]; + /** Durable creation time or first live observation; never reconnect time for Alerts. */ + timestampMs?: number; + /** Live-Worker block identity, retained through committed-row replacement. */ + inFlightBlockId?: number; source: "event"; streaming?: boolean; error?: boolean; @@ -114,6 +126,8 @@ export type ConsoleLine = { toolCallLines?: ConsoleLine[]; /** Typed `SystemItem.kind` used by presentation-only projections. */ systemItemKind?: string; + /** Original protocol Alert, retained for chronology and Alert-specific presentation. */ + alert?: Alert; }; export type InternalWorkerProjection = { @@ -340,18 +354,46 @@ export function mergeCommittedHistoryLines( history: ConsoleLine[], current: ConsoleLine[], ): ConsoleLine[] { - const durableStats = new Map( - history.filter((line) => line.kind === "run_stats" && line.entryId) - .map((line) => [line.entryId!, line]), + const historySource = history.flatMap((line) => line.toolCallLines ?? [line]); + const currentSource = current.flatMap((line) => line.toolCallLines ?? [line]); + const alerts = [...historySource, ...currentSource].filter((line) => + line.alert ); - return aggregateReadToolLines(mergeHistorySourceLines( - history.flatMap((line) => line.toolCallLines ?? [line]), - current.flatMap((line) => line.toolCallLines ?? [line]).map((line) => - line.kind === "run_stats" && line.entryId - ? durableStats.get(line.entryId) ?? line - : line - ), - )); + const durableRows = new Map( + historySource.filter((line) => !line.alert) + .map((line) => [historyLineKey(line), line]), + ); + const merged = mergeHistorySourceLines( + historySource.filter((line) => !line.alert), + currentSource.filter((line) => !line.alert).map((line) => { + const key = historyLineKey(line); + const durable = key ? durableRows.get(key) : undefined; + if (!durable) return line; + if (line.kind === "run_stats") return durable; + // Keep the current row authoritative; borrow durable metadata only when + // needed to place Alerts against a live or partially restored tool row. + if (alerts.length === 0) return line; + return { + ...line, + entryId: line.entryId ?? durable.entryId, + sourceEntryIds: [ + ...new Set([ + ...(durable.sourceEntryIds ?? []), + ...(durable.entryId ? [durable.entryId] : []), + ...(line.sourceEntryIds ?? []), + ...(line.entryId ? [line.entryId] : []), + ]), + ], + timestampMs: line.entryId + ? line.timestampMs ?? durable.timestampMs + : durable.timestampMs ?? line.timestampMs, + }; + }), + ); + return aggregateReadToolLines(orderAlertLines([ + ...merged, + ...alerts, + ])); } function historyLineKey(line: ConsoleLine): string | undefined { @@ -440,12 +482,12 @@ export function projectSessionHistoryEntries( ).lines; } -function projectVisibleConsole( +export function projectVisibleConsole( projection: ConsoleProjection, ): ConsoleProjection { return { ...projection, - lines: aggregateReadToolLines(projection.lines), + lines: aggregateReadToolLines(orderAlertLines(projection.lines)), internalWorkers: projection.internalWorkers.map((worker) => ({ ...worker, console: projectVisibleConsole(worker.console), @@ -650,6 +692,8 @@ function appendSnapshotInFlightLines( blocks: InFlightBlock[], eventId: string, cwd: string | null, + blockIds: number[] = [], + observedAtMs?: number, ): void { blocks.forEach((block, index) => { const blockEventId = `${eventId}:${index}`; @@ -703,6 +747,16 @@ function appendSnapshotInFlightLines( }); break; } + const rowIndex = projection.lines.findLastIndex((line) => + line.eventId === blockEventId + ); + if (rowIndex >= 0) { + projection.lines[rowIndex] = { + ...projection.lines[rowIndex], + inFlightBlockId: blockIds[index], + timestampMs: projection.lines[rowIndex].timestampMs ?? observedAtMs, + }; + } }); } @@ -862,6 +916,65 @@ function appendCommandStream( }; } +function retainSnapshotAlerts( + previous: ConsoleProjection, + restored: ConsoleProjection, +): ConsoleProjection { + const entryIndexes = new Map(); + const blockIndexes = new Map(); + previous.lines.forEach((line, index) => { + const { entryId, inFlightBlockId } = line; + if (entryId !== undefined && !entryIndexes.has(entryId)) { + entryIndexes.set(entryId, index); + } + if (inFlightBlockId !== undefined && !blockIndexes.has(inFlightBlockId)) { + blockIndexes.set(inFlightBlockId, index); + } + }); + const lines = restored.lines.map((line) => { + // Preserve the first matching source row, including when both keys match. + const entryIndex = line.entryId === undefined + ? undefined + : entryIndexes.get(line.entryId); + const blockIndex = line.inFlightBlockId === undefined + ? undefined + : blockIndexes.get(line.inFlightBlockId); + const known = previous.lines[ + Math.min( + entryIndex ?? previous.lines.length, + blockIndex ?? previous.lines.length, + ) + ]; + return known + ? { + ...line, + inFlightBlockId: line.inFlightBlockId ?? known.inFlightBlockId, + timestampMs: line.entryId + ? line.timestampMs + : known.timestampMs ?? line.timestampMs, + } + : line; + }); + const alerts = previous.lines.filter((line) => + line.alert && !(line.error && hasDurableRunFailure(restored, line.body)) + ); + return { + ...restored, + lines: deduplicateAlertLines([...lines, ...alerts]), + internalWorkers: restored.internalWorkers.map((worker) => { + const known = previous.internalWorkers.find((old) => + old.worker.session_id === worker.worker.session_id + ); + return known + ? { + ...worker, + console: retainSnapshotAlerts(known.console, worker.console), + } + : worker; + }), + }; +} + function projectInternalWorkerSnapshot( snapshot: InternalWorkerSnapshot, eventId: string, @@ -882,6 +995,7 @@ function projectInternalWorkerSnapshot( snapshot.in_flight?.blocks ?? [], `${eventId}:internal:${snapshot.worker.session_id}:in-flight`, cwd, + snapshot.in_flight?.block_ids, ); appendSnapshotCommands( console, @@ -1057,7 +1171,10 @@ export function applyProtocolEvent( switch (event.event) { case "user_message": - if (event.data.entry_id && next.lines.some((line) => line.entryId === event.data.entry_id)) break; + if ( + event.data.entry_id && + next.lines.some((line) => line.entryId === event.data.entry_id) + ) break; next.lines.push({ ...line( envelope.eventId, @@ -1072,17 +1189,19 @@ export function applyProtocolEvent( // A snapshot may already include this commit. Check before reconciling so // replay cannot consume a later, unrelated live assistant block. if (isRepeatedSessionEntry(next, event.data.entry)) break; - const replacementIndex = reconcileCommittedSessionEntry( + const replacement = reconcileCommittedSessionEntry( next, event.data.entry, ); applySessionEntry(next, envelope.eventId, event.data.entry); - if (replacementIndex !== undefined) { + if (replacement !== undefined) { + const replacementIndex = replacement.index; const committedIndex = next.lines.findIndex((line) => line.entryId === event.data.entry.entry_id ); if (committedIndex >= 0) { const [committed] = next.lines.splice(committedIndex, 1); + committed.inFlightBlockId = replacement.inFlightBlockId; next.lines.splice(replacementIndex, 0, committed); const stats = next.lines[replacementIndex + 1]; if (stats?.kind === "run_stats" && !stats.entryId) { @@ -1212,6 +1331,8 @@ export function applyProtocolEvent( event.data.in_flight?.blocks ?? [], `${envelope.eventId}:snapshot-in-flight`, next.cwd, + event.data.in_flight?.block_ids, + envelope.observedAtMs, ); appendSnapshotCommands( next, @@ -1221,6 +1342,9 @@ export function applyProtocolEvent( next.internalWorkers = (event.data.internal_workers ?? []).map((worker) => projectInternalWorkerSnapshot(worker, envelope.eventId, next.cwd) ); + const retained = retainSnapshotAlerts(projection, next); + next.lines = retained.lines; + next.internalWorkers = retained.internalWorkers; next.removedInternalWorkers = {}; const withCompaction = applyInFlightCompaction( next, @@ -1278,7 +1402,9 @@ export function applyProtocolEvent( applyCommandEvent(next, envelope.eventId, event.data.event); break; case "segment_rotated": { - const retainedErrors = next.lines.filter((line) => line.kind === "error"); + const retainedErrors = next.lines.filter((line) => + line.kind === "error" || line.alert + ); const retainedCompaction = next.lines.filter((line) => line.id === RUNTIME_COMPACTION_LINE_ID ); @@ -1348,6 +1474,11 @@ export function applyProtocolEvent( break; } + next.lines = next.lines.map((line) => + line.timestampMs === undefined && line.eventId === envelope.eventId + ? { ...line, timestampMs: envelope.observedAtMs } + : line + ); return next; } @@ -1509,8 +1640,9 @@ function appendAlertLine( ): void { const isError = alert.level === "error"; if (isError && hasDurableRunFailure(projection, alert.message)) return; - projection.lines.push( - line( + projection.lines = attachAlertInFlightAnchor(projection.lines, alert); + projection.lines = deduplicateAlertLines([...projection.lines, { + ...line( eventId, isError ? "error" : "status", `Alert · ${alert.source}`, @@ -1519,7 +1651,9 @@ function appendAlertLine( false, isError, ), - ); + alert, + timestampMs: alert.timestamp_ms, + }]); } function findLastLineIndex( @@ -2335,7 +2469,7 @@ function snapshotProjectionFromSession( function reconcileCommittedSessionEntry( projection: ConsoleProjection, entry: SessionSnapshotEntry, -): number | undefined { +): { index: number; inFlightBlockId?: number } | undefined { if (entry.kind !== "message" || entry.role !== "assistant") return; const turnStart = findLastLineIndex( projection.lines, @@ -2348,8 +2482,13 @@ function reconcileCommittedSessionEntry( !line.entryId && line.streaming !== true ) { + projection.lines = resolveCommittedAlertAnchors( + projection.lines, + line.inFlightBlockId, + entry.entry_id, + ); projection.lines.splice(index, 1); - return index; + return { index, inFlightBlockId: line.inFlightBlockId }; } } } @@ -2375,7 +2514,8 @@ function applySessionEntry( if (!isRecord(value) || isRepeatedSessionEntry(projection, value)) return; const eventId = stringField(value, "entry_id") ?? fallbackEventId; const lineCountBefore = projection.lines.length; - switch (stringField(value, "kind")) { + const entryKind = stringField(value, "kind"); + switch (entryKind) { case "user_input": projection.lines.push( line( @@ -2422,7 +2562,10 @@ function applySessionEntry( const requests = safeTokenCount(value["requests"]); const upload = safeTokenCount(value["upload_tokens"]); const output = safeTokenCount(value["output_tokens"]); - if (elapsed !== null && requests !== null && upload !== null && output !== null) { + if ( + elapsed !== null && requests !== null && upload !== null && + output !== null + ) { projection.lines.push(runStatsLine(eventId, { startedAtMs: 0, requests, @@ -2451,18 +2594,41 @@ function applySessionEntry( break; } const entryId = stringField(value, "entry_id"); - if (entryId) { + const timestampMs = typeof value["timestamp"] === "number" + ? value["timestamp"] + : undefined; + const stampLine = (index: number) => { + const line = projection.lines[index]; + if (!line || line.eventId !== eventId) return; + const sourceEntryIds = new Set(line.sourceEntryIds ?? []); + if (line.entryId) sourceEntryIds.add(line.entryId); + if (entryId) sourceEntryIds.add(entryId); + projection.lines[index] = { + ...line, + entryId: entryId ?? line.entryId, + sourceEntryIds: [...sourceEntryIds], + timestampMs: entryKind === "tool_result" + ? line.timestampMs ?? timestampMs + : timestampMs ?? line.timestampMs, + }; + }; + if (entryKind === "run_error") { + // Failure reconciliation can remove transient rows before appending. + stampLine(projection.lines.length - 1); + } else if ( + (entryKind === "tool_call" || entryKind === "tool_result") && + projection.lines.length === lineCountBefore + ) { + stampLine( + projection.lines.findLastIndex((line) => line.eventId === eventId), + ); + } else { for ( - let index = Math.max( - 0, - Math.min(lineCountBefore, projection.lines.length - 1), - ); + let index = lineCountBefore; index < projection.lines.length; index += 1 ) { - if (projection.lines[index].eventId === eventId) { - projection.lines[index].entryId = entryId; - } + stampLine(index); } } } diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/console-display.browser.spec.ts b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/console-display.browser.spec.ts index 75a312224..207df74bd 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/console-display.browser.spec.ts +++ b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/console-display.browser.spec.ts @@ -1268,6 +1268,55 @@ test("preserves rendered content while reconnecting", async () => { ).not.toBeNull(); }); +test("Alert keeps its transcript position on reconnect and a fresh Console mount", async () => { + vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => + Promise.resolve(Response.json(String(input).includes("/session/history") + ? emptyHistoryPage() + : { availability: "live_protocol" })) + )); + const first = sessionWithUserMessage("before the alert"); + const second = sessionWithUserMessage("after the alert").entries[0]; + const full = { ...first, entries: [...first.entries, second] }; + const alert: ProtocolEvent = { + event: "alert", + data: { + id: "alert-reopen-test", + position: { kind: "after_entry", entry_id: first.entries[0].entry_id }, + level: "warn", source: "compactor", message: "retained alert body", timestamp_ms: 2, + }, + }; + const expectOrder = () => { + const article = screen.getByRole("article", { name: "main transcript" }); + const rows = Array.from(article.querySelectorAll(".console-line")); + expect(rows).toHaveLength(3); + expect(rows[0].textContent).toContain("before the alert"); + expect(rows[1].textContent).toContain("retained alert body"); + expect(rows[1].classList.contains("alert-line")).toBe(true); + expect(rows[2].textContent).toContain("after the alert"); + }; + const initial = render(ConsolePage, { data: pageData() }); + await waitFor(() => expect(multiplexer.subscribe).toHaveBeenCalledOnce()); + latestListener().onFrame(subscribedFrame(first)); + protocolEvent(alert); + protocolEvent({ event: "user_message", data: { + entry_id: second.entry_id, segments: [{ kind: "text", content: "after the alert" }], + } }); + await waitFor(expectOrder); + + latestListener().onStatus?.("closed"); + latestListener().onStatus?.("connecting"); + const replay = subscribedFrame(full); + replay.message.payload.snapshot.data.events.push(alert, alert); + latestListener().onFrame(replay); + await waitFor(expectOrder); + + initial.unmount(); + render(ConsolePage, { data: pageData() }); + await waitFor(() => expect(multiplexer.subscribe).toHaveBeenCalledTimes(2)); + latestListener().onFrame(replay); + await waitFor(expectOrder); +}); + test("reconnect snapshot refreshes lineage and removes stale sibling history", async () => { let historyRequests = 0; vi.stubGlobal(