From e8dfb3e68dc80b34da2e42511b16e4ad7d02fc07 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 02:02:28 +0900 Subject: [PATCH 01/11] refactor: notify system input producers after durable commit --- crates/worker/src/feature/builtin/memory.rs | 56 ++++++++-- crates/worker/src/ipc/interceptor.rs | 112 +++++++++++++++++--- crates/worker/src/worker.rs | 94 ++++++++++++++-- 3 files changed, 225 insertions(+), 37 deletions(-) diff --git a/crates/worker/src/feature/builtin/memory.rs b/crates/worker/src/feature/builtin/memory.rs index c4973cea5..32155ea04 100644 --- a/crates/worker/src/feature/builtin/memory.rs +++ b/crates/worker/src/feature/builtin/memory.rs @@ -691,6 +691,13 @@ impl SystemPromptContributionSource for WorkspaceResidentSummarySource { } impl WorkspaceSubjektivResidentSummarySource { + fn confirm_resident_context_behavior(&self, behavior_md: &str) { + self.behavior_state + .lock() + .expect("subject behavior state poisoned") + .applied = Some(behavior_md.to_owned()); + } + async fn fetch( &self, ) -> Result { @@ -788,11 +795,14 @@ impl SystemPromptContributionSource for WorkspaceSubjektivResidentSummarySource })) } - fn confirm_resident_context_behavior(&self, behavior_md: &str) { - self.behavior_state - .lock() - .expect("subject behavior state poisoned") - .applied = Some(behavior_md.to_owned()); + fn system_item_committed( + &self, + item: &session_store::SystemItem, + _metadata: &crate::session_history::SessionHistoryMetadata, + ) { + if let session_store::SystemItem::SubjectBehaviorRefresh { behavior_md, .. } = item { + self.confirm_resident_context_behavior(behavior_md); + } } fn invalidate_resident_context_representation(&self) { @@ -1741,7 +1751,14 @@ permission = "write" .expect("changed behavior must produce one refresh"); assert_eq!(refresh.behavior_md, "Ask when uncertain."); assert!(refresh.body.contains("Ask when uncertain.")); - source.confirm_resident_context_behavior(&refresh.behavior_md); + source.system_item_committed( + &session_store::SystemItem::SubjectBehaviorRefresh { + body: refresh.body.clone(), + behavior_md: refresh.behavior_md.clone(), + prompt_provenance: None, + }, + &session_store::LoggedSessionHistoryMetadata::legacy_unknown(), + ); assert!( source .load_changed_resident_context() @@ -1758,7 +1775,14 @@ permission = "write" .expect("cleared behavior must produce one refresh"); assert_eq!(cleared.behavior_md, ""); assert!(cleared.body.contains("No user-managed behavior is set")); - source.confirm_resident_context_behavior(&cleared.behavior_md); + source.system_item_committed( + &session_store::SystemItem::SubjectBehaviorRefresh { + body: cleared.body.clone(), + behavior_md: cleared.behavior_md.clone(), + prompt_provenance: None, + }, + &session_store::LoggedSessionHistoryMetadata::legacy_unknown(), + ); assert!( source .load_changed_resident_context() @@ -1816,7 +1840,14 @@ permission = "write" .await .unwrap() .unwrap(); - source.confirm_resident_context_behavior(&first.behavior_md); + source.system_item_committed( + &session_store::SystemItem::SubjectBehaviorRefresh { + body: first.body.clone(), + behavior_md: first.behavior_md.clone(), + prompt_provenance: None, + }, + &session_store::LoggedSessionHistoryMetadata::legacy_unknown(), + ); assert_eq!( source .load_changed_resident_context() @@ -1825,7 +1856,14 @@ permission = "write" .unwrap(), second ); - source.confirm_resident_context_behavior(&second.behavior_md); + source.system_item_committed( + &session_store::SystemItem::SubjectBehaviorRefresh { + body: second.body.clone(), + behavior_md: second.behavior_md.clone(), + prompt_provenance: None, + }, + &session_store::LoggedSessionHistoryMetadata::legacy_unknown(), + ); assert!( source .load_changed_resident_context() diff --git a/crates/worker/src/ipc/interceptor.rs b/crates/worker/src/ipc/interceptor.rs index be4f18dda..cbb500796 100644 --- a/crates/worker/src/ipc/interceptor.rs +++ b/crates/worker/src/ipc/interceptor.rs @@ -216,15 +216,6 @@ impl WorkerInterceptor { self } - fn confirm_resident_context_item(&self, item: &SystemItem) { - let Some(source) = self.resident_context_source.as_ref() else { - return; - }; - if let SystemItem::SubjectBehaviorRefresh { behavior_md, .. } = item { - source.confirm_resident_context_behavior(behavior_md); - } - } - /// Commit each `SystemItem` as its own `LogEntry::AnnotatedSystemItem` /// entry through the attached writer (no-op when no writer is /// wired). Sync — writes complete before the matching @@ -243,16 +234,20 @@ impl WorkerInterceptor { return Ok(()); }; for (item, extensions, history_provenance) in items { - let entry = writer.commit_system_item_with_extensions( + writer.commit_system_item_with_extensions_then( item.clone(), extensions.clone(), history_provenance.clone(), + &mut |entry| { + self.pending_committed_history + .lock() + .expect("pending committed history poisoned") + .push_back(entry.clone()); + if let Some(source) = self.resident_context_source.as_ref() { + source.system_item_committed(item, &entry.annotation); + } + }, )?; - self.pending_committed_history - .lock() - .expect("pending committed history poisoned") - .push_back(entry); - self.confirm_resident_context_item(item); } Ok(()) } @@ -1003,8 +998,14 @@ mod tests { SystemPromptContribution::Unavailable } - fn confirm_resident_context_behavior(&self, behavior_md: &str) { - self.confirmed.lock().unwrap().push(behavior_md.to_owned()); + fn system_item_committed( + &self, + item: &SystemItem, + _metadata: &crate::session_history::SessionHistoryMetadata, + ) { + if let SystemItem::SubjectBehaviorRefresh { behavior_md, .. } = item { + self.confirmed.lock().unwrap().push(behavior_md.to_owned()); + } } } @@ -1556,6 +1557,83 @@ mod tests { )); } + #[test] + fn system_input_completion_reports_only_durable_batch_prefix() { + struct PrefixCommitter(Arc>>); + impl SystemItemCommitter for PrefixCommitter { + fn commit_log_entry( + &self, + entry: session_store::LogEntry, + ) -> Result<(), session_store::StoreError> { + let mut committed = self.0.lock().unwrap(); + if !committed.is_empty() { + return Err(std::io::Error::other("injected second append failure").into()); + } + committed.push(entry); + Ok(()) + } + } + struct Observer { + saved: Arc>>, + receipts: Arc>>, + } + #[async_trait] + impl SystemPromptContributionSource for Observer { + async fn load(&self) -> SystemPromptContribution { + SystemPromptContribution::Unavailable + } + fn system_item_committed( + &self, + item: &SystemItem, + metadata: &crate::session_history::SessionHistoryMetadata, + ) { + let saved = self.saved.lock().unwrap(); + let Some(session_store::LogEntry::AnnotatedSystemItem { entry, .. }) = saved.last() + else { + panic!("completion must follow persistence"); + }; + assert_eq!(&entry.metadata, metadata); + assert_eq!(entry.item.history_text(), item.history_text()); + self.receipts.lock().unwrap().push(metadata.clone()); + } + } + let saved = Arc::new(Mutex::new(Vec::new())); + let receipts = Arc::new(Mutex::new(Vec::new())); + let pending = Arc::new(Mutex::new(Vec::new())); + let interceptor = WorkerInterceptor::new( + Arc::new(HookRegistryBuilder::new().build()), + None, + None, + NotifyBuffer::new(), + pending.clone(), + test_prompts(), + Some(Arc::new(PrefixCommitter(saved.clone()))), + ) + .with_resident_context_source(Some(Arc::new(Observer { + saved: saved.clone(), + receipts: receipts.clone(), + }))); + let items = [ + SystemItem::Notification { + message: "first".into(), + body: "first saved input".into(), + prompt_provenance: None, + }, + SystemItem::FileAttachment { + path: "test.rs".into(), + body: "second unsaved input".into(), + }, + ]; + assert!(interceptor.commit_system_items(&items).is_err()); + assert_eq!(saved.lock().unwrap().len(), 1); + let receipts = receipts.lock().unwrap(); + assert_eq!(receipts.len(), 1); + let pending = interceptor.pending_committed_history.lock().unwrap(); + assert_eq!(pending.len(), 1); + assert_eq!(pending[0].annotation, receipts[0]); + assert_eq!(pending[0].item.as_text(), Some("first saved input")); + } + #[test] fn behavior_ack_uses_raw_content_including_clear_not_rendered_context() { let committed = Arc::new(Mutex::new(Vec::new())); diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 6af8d337b..a60803948 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -2733,22 +2733,40 @@ pub trait SystemItemCommitter: Send + Sync { item: SystemItem, extensions: Vec, history_provenance: Option, + ) -> Result, StoreError> { + self.commit_system_item_with_extensions_then( + item, + extensions, + history_provenance, + &mut |_| {}, + ) + } + + /// Notify the producer inside the publication barrier, after the durable + /// append and before a rewrite can invalidate its representation. + fn commit_system_item_with_extensions_then( + &self, + item: SystemItem, + extensions: Vec, + history_provenance: Option, + after_commit: &mut dyn FnMut(&HistoryEntry), ) -> Result, StoreError> { let metadata = new_history_metadata( history_provenance .unwrap_or(WorkerHistoryProvenance::BackendInstruction { operation_id: None }), None, ); - let history_item = item.to_history_item(); - self.commit_log_entry(LogEntry::AnnotatedSystemItem { - ts: segment_log::now_millis(), - entry: session_store::LoggedSystemHistoryEntry { - item, - metadata: metadata.clone(), + let history = HistoryEntry::new(item.to_history_item(), metadata.clone()); + self.commit_usage_boundary( + LogEntry::AnnotatedSystemItem { + ts: segment_log::now_millis(), + entry: session_store::LoggedSystemHistoryEntry { item, metadata }, + extensions, }, - extensions, - })?; - Ok(HistoryEntry::new(history_item, metadata)) + &mut || after_commit(&history), + &[], + )?; + Ok(history) } } @@ -2937,8 +2955,11 @@ pub(crate) trait SystemPromptContributionSource: Send + Sync { Ok(None) } - /// Acknowledges the exact behavior represented by a durably committed refresh. - fn confirm_resident_context_behavior(&self, _behavior_md: &str) {} + /// Reports a live durable system-input commit to its producer. Interpretation + /// belongs to the Feature; restoration never invokes this notification. + /// Runs under the publication barrier: only bounded local state updates are + /// allowed, without Host I/O or reentry into history mutation. + fn system_item_committed(&self, _item: &SystemItem, _metadata: &SessionHistoryMetadata) {} /// Invalidates the in-memory representation fence before history may be /// compacted or rewound. @@ -17436,6 +17457,57 @@ permission = "write" } } + #[test] + fn system_input_completion_observes_durable_identity_under_publication_barrier() { + let temp = tempfile::tempdir().unwrap(); + let store = session_store::FsStore::new(temp.path()).unwrap(); + let session_id = SessionId::now_v7(); + let segment_id = SegmentId::now_v7(); + let start = LogEntry::AnnotatedSegmentStart { + ts: 1, + session_id, + system_prompt: None, + config: agen::llm_client::RequestConfig::default(), + history: vec![], + forked_from: None, + compacted_from: None, + }; + store + .create_segment(session_id, segment_id, std::slice::from_ref(&start)) + .unwrap(); + let writer = LogWriterHandle { + store: store.clone(), + state: SegmentState::new(session_id, segment_id, 1), + sink: SegmentLogSink::with_initial(vec![start]), + in_flight: None, + }; + let mut receipt = None; + let committed = writer + .commit_system_item_with_extensions_then( + SystemItem::Notification { + message: "message".into(), + body: "exact committed system body".into(), + prompt_provenance: None, + }, + vec![], + None, + &mut |history| { + assert!(writer.state.append_lock.try_lock().is_err()); + let durable = store.read_all(session_id, segment_id).unwrap(); + let Some(LogEntry::AnnotatedSystemItem { entry, .. }) = durable.last() else { + panic!("completion must follow actual storage append"); + }; + assert_eq!(entry.metadata, history.annotation); + assert_eq!(entry.item.to_history_item(), history.item); + assert_eq!(history.item.as_text(), Some("exact committed system body")); + receipt = Some(history.annotation.entry_id.clone()); + }, + ) + .unwrap(); + assert_eq!(receipt, Some(committed.annotation.entry_id)); + assert!(writer.state.append_lock.try_lock().is_ok()); + } + fn minimal_manifest() -> WorkerManifest { let toml_str = r#" [worker] From cd0eb5eb004f7c5ae77504d0eb20d797553b5a62 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 02:02:28 +0900 Subject: [PATCH 02/11] fix: allowlist public system input projections --- crates/protocol/src/lib.rs | 10 +-- crates/session-store/src/lib.rs | 3 +- crates/session-store/src/public_snapshot.rs | 16 ++-- crates/session-store/src/system_item.rs | 76 +++++++++++++++++++ crates/tui/src/app.rs | 54 ++++++++----- crates/worker/src/ipc/protocol_session.rs | 71 ++++++++++++++++- crates/worker/tests/controller_test.rs | 2 +- .../src/lib/generated/protocol-validator.ts | 2 +- .../src/lib/workspace/console/model.test.ts | 1 + 9 files changed, 194 insertions(+), 41 deletions(-) diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index f7baece98..94f04a182 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -1180,12 +1180,10 @@ pub enum Event { }, /// One agent-injected system item committed to history. /// - /// Carries the JSON form of `session_store::SystemItem`. Covers - /// `Method::Notify` echoes, child-Worker lifecycle events from - /// `Method::WorkerEvent`, `@` resolution payloads, and any future - /// Clients dispatch on the `kind` tag for typed rendering instead - /// of parsing free-text prefixes like `[Notification] …` or - /// `[File: …]`. + /// Carries the allowlisted JSON form of `session_store::PublicSystemItem`, + /// not the storage record. Its descriptive `kind` is independent from the + /// typed `presentation` used for notification, reminder, or context rendering. + /// Private Feature payloads and prompt provenance are never event data. /// /// One event per `LogEntry::AnnotatedSystemItem` commit. Disk-side and /// wire-side are 1:1. diff --git a/crates/session-store/src/lib.rs b/crates/session-store/src/lib.rs index a164159d9..ef8d8111c 100644 --- a/crates/session-store/src/lib.rs +++ b/crates/session-store/src/lib.rs @@ -98,7 +98,8 @@ pub use segment::{ pub use segment_log::{LogEntry, RestoredState, SegmentOrigin, SessionExtension, collect_state}; pub use store::{Store, StoreError}; pub use system_item::{ - PromptRenderProvenance, SystemItem, SystemReminder, SystemReminderSource, render_worker_event, + PromptRenderProvenance, PublicSystemItem, PublicSystemPresentation, SystemItem, SystemReminder, + SystemReminderSource, render_worker_event, }; pub use uploaded_file::{ DEFAULT_MAX_FILES_PER_SUBMISSION, DEFAULT_MAX_SESSION_ARTIFACT_BYTES, diff --git a/crates/session-store/src/public_snapshot.rs b/crates/session-store/src/public_snapshot.rs index 824d7dffd..6a1d94b0e 100644 --- a/crates/session-store/src/public_snapshot.rs +++ b/crates/session-store/src/public_snapshot.rs @@ -1992,16 +1992,10 @@ fn system_entry( provenance: SessionEntryProvenance, derived_from: Vec, ) -> SessionSnapshotEntry { - let mut data = serde_json::to_value(item).ok(); - if let Some(serde_json::Value::Object(object)) = data.as_mut() { - object.remove("prompt_provenance"); - } - let item_kind = data - .as_ref() - .and_then(|value| value.get("kind")) - .and_then(serde_json::Value::as_str) - .unwrap_or("system_item") - .to_owned(); + let public = item.public_item(); + let data = Some(serde_json::to_value(&public).expect("public system input is serializable")); + let item_kind = public.kind; + let content = public.body; SessionSnapshotEntry { entry_id, timestamp, @@ -2009,7 +2003,7 @@ fn system_entry( derived_from, data: SessionSnapshotEntryData::SystemItem { item_kind, - content: item.history_text(), + content, data, }, } diff --git a/crates/session-store/src/system_item.rs b/crates/session-store/src/system_item.rs index 54b815bda..be201e5cc 100644 --- a/crates/session-store/src/system_item.rs +++ b/crates/session-store/src/system_item.rs @@ -197,7 +197,58 @@ pub enum SystemItem { }, } +/// Allowlisted presentation data shared by live events and retained snapshots. +/// Private Feature payloads and render provenance never cross this boundary. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PublicSystemItem { + pub kind: String, + pub body: String, + pub presentation: PublicSystemPresentation, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum PublicSystemPresentation { + Notification { message: String }, + WorkerEvent { event: WorkerEvent }, + Context, + TaskReminder, + Ignored, +} + impl SystemItem { + pub fn public_item(&self) -> PublicSystemItem { + let presentation = match self { + Self::Notification { message, .. } => PublicSystemPresentation::Notification { + message: message.clone(), + }, + Self::WorkerEvent { event, .. } => PublicSystemPresentation::WorkerEvent { + event: event.clone(), + }, + Self::TaskReminder { .. } => PublicSystemPresentation::TaskReminder, + Self::LegacyKnowledgeIgnored { .. } | Self::LegacyIgnored { .. } => { + PublicSystemPresentation::Ignored + } + Self::JournalStartup { .. } + | Self::ResidentSummaryRefresh { .. } + | Self::SubjectBehaviorRefresh { .. } + | Self::SkillActivation { .. } + | Self::FeatureInvocationResult { .. } + | Self::FileAttachment { .. } + | Self::Interrupt { .. } => PublicSystemPresentation::Context, + }; + PublicSystemItem { + kind: self.kind_label().to_owned(), + body: if matches!(presentation, PublicSystemPresentation::Ignored) { + String::new() + } else { + self.history_text() + }, + presentation, + } + } + /// Free-text body the LLM sees inside its `role:system` message /// for this item. Returns the variant's stored `body` verbatim. pub fn history_text(&self) -> String { @@ -311,6 +362,31 @@ mod tests { ); } + #[test] + fn public_system_input_decoder_rejects_private_fields_and_unknown_presentations() { + let valid = serde_json::json!({ + "kind": "new-feature-label", + "body": "explicit public content", + "presentation": { "kind": "context" }, + }); + let public: PublicSystemItem = serde_json::from_value(valid.clone()).unwrap(); + assert_eq!(public.body, "explicit public content"); + for private_field in [ + "payload", + "prompt_provenance", + "policy", + "snapshot", + "behavior_md", + ] { + let mut malformed = valid.clone(); + malformed[private_field] = serde_json::json!({ "private": true }); + assert!(serde_json::from_value::(malformed).is_err()); + } + let mut unknown = valid; + unknown["presentation"] = serde_json::json!({ "kind": "execute_feature_payload" }); + assert!(serde_json::from_value::(unknown).is_err()); + } + #[test] fn frozen_subject_behavior_log_reads_old_counter_without_republishing_it() { let old = r#"{"kind":"subject_behavior_refresh","body":"exact archived body","behavior_revision":7}"#; diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index a24aaf339..306368070 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -3013,7 +3013,8 @@ impl App { /// `[File: …]` parsing path: each kind maps directly to a typed /// block (`Block::Notify`, `Block::WorkerEvent`, …). fn apply_system_item(&mut self, value: &serde_json::Value) { - let Ok(item) = serde_json::from_value::(value.clone()) else { + let Ok(item) = serde_json::from_value::(value.clone()) + else { // Unknown / forward-compat shape: fall back to rendering the // raw text payload (if any) as a generic system message. if let Some(text) = value.get("body").and_then(|b| b.as_str()) { @@ -3024,29 +3025,23 @@ impl App { } return; }; - match item { - session_store::SystemItem::Notification { message, .. } => { + let body = item.body; + match item.presentation { + session_store::PublicSystemPresentation::Notification { message } => { self.blocks.push(Block::Notify { message }); } - session_store::SystemItem::WorkerEvent { event, .. } => { + session_store::PublicSystemPresentation::WorkerEvent { event } => { self.blocks.push(Block::WorkerEvent { event }); } - session_store::SystemItem::FeatureInvocationResult { body, .. } - | session_store::SystemItem::FileAttachment { body, .. } - | session_store::SystemItem::SkillActivation { body, .. } - | session_store::SystemItem::JournalStartup { body, .. } - | session_store::SystemItem::ResidentSummaryRefresh { body, .. } - | session_store::SystemItem::SubjectBehaviorRefresh { body, .. } - | session_store::SystemItem::Interrupt { body, .. } => { + session_store::PublicSystemPresentation::Context => { self.task_store.apply_system_message_text(&body); self.blocks.push(Block::SystemMessage { text: body }); } - session_store::SystemItem::TaskReminder { body, .. } => { + session_store::PublicSystemPresentation::TaskReminder => { self.task_store.apply_system_message_text(&body); self.blocks.push(Block::TaskReminder { text: body }); } - session_store::SystemItem::LegacyIgnored { .. } => {} - session_store::SystemItem::LegacyKnowledgeIgnored { .. } => {} + session_store::PublicSystemPresentation::Ignored => {} } } @@ -4450,12 +4445,12 @@ mod completion_flow_tests { } #[test] - fn live_legacy_workflow_system_item_is_ignored() { + fn retired_system_input_is_ignored() { let mut app = App::new("test".into()); let item = serde_json::json!({ - "kind": "workflow", - "slug": "build", - "body": "[Workflow /build]\nRun the build", + "kind": "retired_input", + "body": "", + "presentation": { "kind": "ignored" }, }); app.handle_worker_event(Event::SystemItem { entry_id: None, @@ -4861,12 +4856,30 @@ mod completion_flow_tests { ); } + #[test] + fn feature_label_does_not_select_system_input_presentation() { + for label in ["journal_startup", "new-unregistered-feature"] { + let mut app = App::new("test".into()); + app.handle_worker_event(Event::SystemItem { + entry_id: None, + item: serde_json::json!({ + "kind": label, + "body": "explicit public content", + "presentation": { "kind": "context" }, + }), + }); + assert!( + matches!(app.blocks.as_slice(), [Block::SystemMessage { text }] if text == "explicit public content") + ); + } + } + #[test] fn live_system_item_notification_appends_notify_block() { let mut app = App::new("test".into()); let item = serde_json::json!({ "kind": "notification", - "message": "hi", + "presentation": { "kind": "notification", "message": "hi" }, "body": "[Notification] hi", }); app.handle_worker_event(Event::SystemItem { @@ -4884,7 +4897,7 @@ mod completion_flow_tests { let mut app = App::new("test".into()); let item = serde_json::json!({ "kind": "worker_event", - "event": { "kind": "turn_ended", "worker_name": "child" }, + "presentation": { "kind": "worker_event", "event": { "kind": "turn_ended", "worker_name": "child" } }, "body": "[Notification] worker `child` finished a turn", }); app.handle_worker_event(Event::SystemItem { @@ -5285,6 +5298,7 @@ mod completion_flow_tests { item: serde_json::json!({ "kind": "task_reminder", "body": snapshot, + "presentation": { "kind": "task_reminder" }, }), }); diff --git a/crates/worker/src/ipc/protocol_session.rs b/crates/worker/src/ipc/protocol_session.rs index 6735afdcf..759c9c411 100644 --- a/crates/worker/src/ipc/protocol_session.rs +++ b/crates/worker/src/ipc/protocol_session.rs @@ -90,7 +90,8 @@ pub fn live_log_entry_event(entry: LogEntry) -> Option { } LogEntry::AnnotatedSystemItem { entry, .. } => { let entry_id = Some(entry.metadata.entry_id.0.clone()); - let value = serde_json::to_value(&entry.item).expect("SystemItem is Serialize"); + let value = serde_json::to_value(entry.item.public_item()) + .expect("public system input is serializable"); Some(Event::SystemItem { entry_id, item: value, @@ -370,6 +371,74 @@ mod tests { } } + #[test] + fn live_and_snapshot_system_projection_exclude_private_feature_fields() { + let provenance = session_store::PromptRenderProvenance { + workspace_id: Some("private-workspace".into()), + source_digest: "private-source".into(), + projection_digest: "private-projection".into(), + logical_name: "private-template".into(), + }; + let items = [ + session_store::SystemItem::SubjectBehaviorRefresh { + body: "displayed context".into(), + behavior_md: "private-raw-behavior".into(), + prompt_provenance: Some(provenance.clone()), + }, + session_store::SystemItem::JournalStartup { + body: "displayed reference".into(), + snapshot: serde_json::json!({ "internal": "private-snapshot" }), + snapshot_sha256: "private-snapshot-digest".into(), + prompt_provenance: provenance, + }, + ]; + for item in items { + let entry = LogEntry::AnnotatedSystemItem { + ts: 7, + entry: session_store::LoggedSystemHistoryEntry { + item, + metadata: session_store::LoggedSessionHistoryMetadata::legacy_unknown(), + }, + extensions: vec![], + }; + let persisted: LogEntry = + serde_json::from_slice(&serde_json::to_vec(&entry).unwrap()).unwrap(); + let snapshot = session_store::public_snapshot::project_current_session_snapshot(&[ + persisted.clone(), + ]); + let Event::SystemItem { + entry_id, + item: live, + } = live_log_entry_event(persisted).unwrap() + else { + panic!("expected live system input"); + }; + let snapshot_entry = &snapshot.entries[0]; + assert_eq!(entry_id.as_deref(), Some(snapshot_entry.entry_id.as_str())); + let protocol::SessionSnapshotEntryData::SystemItem { + data: Some(saved), + content, + .. + } = &snapshot_entry.data + else { + panic!("expected canonical public system input"); + }; + assert_eq!(&live, saved); + let public: session_store::PublicSystemItem = + serde_json::from_value(live.clone()).unwrap(); + assert_eq!(&public.body, content); + assert!(matches!( + public.presentation, + session_store::PublicSystemPresentation::Context + )); + let encoded = serde_json::to_string(&snapshot).unwrap(); + assert!(!encoded.contains("private-")); + assert!(live.get("prompt_provenance").is_none()); + assert!(live.get("snapshot").is_none()); + assert!(live.get("behavior_md").is_none()); + } + } + #[test] fn user_input_log_entry_maps_to_user_message_event() { let segments = vec![protocol::Segment::text("hello from log")]; diff --git a/crates/worker/tests/controller_test.rs b/crates/worker/tests/controller_test.rs index 1938f0b18..43c1fdfb6 100644 --- a/crates/worker/tests/controller_test.rs +++ b/crates/worker/tests/controller_test.rs @@ -3087,7 +3087,7 @@ async fn socket_worker_event_turn_ended_while_idle_auto_starts_turn() { Ok(Some(Event::SystemItem { ref item, .. })) if item.get("kind").and_then(|k| k.as_str()) == Some("worker_event") && item - .pointer("/event/worker_name") + .pointer("/presentation/event/worker_name") .and_then(|v| v.as_str()) == Some("child") => { saw_worker_event_echo = true; diff --git a/web/workspace/src/lib/generated/protocol-validator.ts b/web/workspace/src/lib/generated/protocol-validator.ts index b75fa7738..2a2da1ea5 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":{"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"}; +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 allowlisted JSON form of `session_store::PublicSystemItem`,\nnot the storage record. Its descriptive `kind` is independent from the\ntyped `presentation` used for notification, reminder, or context rendering.\nPrivate Feature payloads and prompt provenance are never event data.\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/workspace/console/model.test.ts b/web/workspace/src/lib/workspace/console/model.test.ts index ad4ec795d..11cfe60ad 100644 --- a/web/workspace/src/lib/workspace/console/model.test.ts +++ b/web/workspace/src/lib/workspace/console/model.test.ts @@ -3943,6 +3943,7 @@ Deno.test("live system items reconcile with committed history by entry identity" const item = { kind: "resident_summary_refresh", body: "Resident memory refresh", + presentation: { kind: "context" }, }; const event: Event = { event: "system_item", From 887184796e4e5ba02ce1b7e3f889a3bffcb863e5 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 02:05:02 +0900 Subject: [PATCH 03/11] docs: distinguish commit completion from Feature authority --- crates/worker/src/worker.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index a60803948..fdde548d2 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -2742,8 +2742,9 @@ pub trait SystemItemCommitter: Send + Sync { ) } - /// Notify the producer inside the publication barrier, after the durable - /// append and before a rewrite can invalidate its representation. + /// Notify after durable append through the writer's publication boundary. + /// Completion is a storage fact, not a grant of continuing Feature authority; + /// producers must separately fence their representation lifetime. fn commit_system_item_with_extensions_then( &self, item: SystemItem, From 19514a5d6a2777ed0bad64443ed057b73f31a2e5 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 02:14:52 +0900 Subject: [PATCH 04/11] feat: define bounded Feature-owned system input envelopes --- crates/protocol/src/lib.rs | 2 +- crates/session-store/src/lib.rs | 5 + crates/session-store/src/system_input.rs | 530 +++++++++++++++++++++++ crates/session-store/src/system_item.rs | 4 +- 4 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 crates/session-store/src/system_input.rs diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 94f04a182..8055193d5 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -408,7 +408,7 @@ pub enum Method { /// Transport is fire-and-forget; receivers must tolerate out-of-order /// delivery (e.g. `TurnEnded` arriving after `ShutDown` for the same /// child Worker). -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] #[serde(tag = "kind", rename_all = "snake_case")] diff --git a/crates/session-store/src/lib.rs b/crates/session-store/src/lib.rs index ef8d8111c..7750f87e0 100644 --- a/crates/session-store/src/lib.rs +++ b/crates/session-store/src/lib.rs @@ -51,6 +51,7 @@ pub mod retention; pub mod segment; pub mod segment_log; pub mod store; +pub mod system_input; pub mod system_item; pub mod uploaded_file; pub mod worker_metadata; @@ -97,6 +98,10 @@ pub use segment::{ }; pub use segment_log::{LogEntry, RestoredState, SegmentOrigin, SessionExtension, collect_state}; pub use store::{Store, StoreError}; +pub use system_input::{ + FeatureInputPayload, InputCompactionUse, InputEvidenceUse, InputObservationUse, + SystemInputEnvelope, SystemInputError, SystemInputMetadata, SystemInputUsePolicy, +}; pub use system_item::{ PromptRenderProvenance, PublicSystemItem, PublicSystemPresentation, SystemItem, SystemReminder, SystemReminderSource, render_worker_event, diff --git a/crates/session-store/src/system_input.rs b/crates/session-store/src/system_input.rs new file mode 100644 index 000000000..fc9613941 --- /dev/null +++ b/crates/session-store/src/system_input.rs @@ -0,0 +1,530 @@ +//! Validated, Feature-neutral system-input data. +//! +//! The enclosing history annotation owns identity, origin and derivation. None of +//! those descriptive facts selects a use policy here. Model text, independent use +//! policies, private Feature data and an explicit public projection travel as one +//! value. Restoring this value does not load a Feature or invoke domain behavior. +//! +//! Payload validation bounds the decoded value; enclosing log readers must also +//! enforce their record allocation limit before decoding untrusted persisted input. + +use agen::Item; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::PublicSystemItem; + +pub const MAX_FEATURE_PAYLOAD_BYTES: usize = 128 * 1024; +pub const MAX_SYSTEM_INPUT_BYTES: usize = 4 * 1024 * 1024; +pub const MAX_SYSTEM_PUBLIC_BYTES: usize = 256 * 1024; +const MAX_SCHEMA_BYTES: usize = 128; +const MAX_JSON_DEPTH: usize = 16; +const MAX_JSON_NODES: usize = 8192; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum SystemInputError { + #[error("system input payload schema is invalid")] + InvalidSchema, + #[error("system input payload version must be nonzero")] + InvalidVersion, + #[error("system input resource limit exceeded")] + ResourceLimit, + #[error("system input payload could not be serialized")] + Serialization, + #[error("system input payload schema or version is unsupported by this consumer")] + UnsupportedPayload, + #[error("system input payload does not match the owner's typed contract")] + InvalidTypedPayload, +} + +/// One private payload owned by a Feature's versioned typed decoder. +/// Unknown valid schemas/versions remain data, never execution instructions. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "PayloadWire")] +pub struct FeatureInputPayload { + schema: String, + version: u32, + value: Value, +} + +impl std::fmt::Debug for FeatureInputPayload { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FeatureInputPayload") + .field("schema", &self.schema) + .field("version", &self.version) + .finish_non_exhaustive() + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct PayloadWire { + schema: String, + version: u32, + value: Value, +} + +impl TryFrom for FeatureInputPayload { + type Error = SystemInputError; + + fn try_from(wire: PayloadWire) -> Result { + if wire.schema.is_empty() + || wire.schema.len() > MAX_SCHEMA_BYTES + || !wire.schema.as_bytes()[0].is_ascii_lowercase() + || !wire.schema.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"-_.:/".contains(&byte) + }) + { + return Err(SystemInputError::InvalidSchema); + } + if wire.version == 0 { + return Err(SystemInputError::InvalidVersion); + } + validate_json(&wire.value, MAX_FEATURE_PAYLOAD_BYTES)?; + Ok(Self { + schema: wire.schema, + version: wire.version, + value: wire.value, + }) + } +} + +impl FeatureInputPayload { + pub fn encode( + schema: impl Into, + version: u32, + value: &T, + ) -> Result { + PayloadWire { + schema: schema.into(), + version, + value: serde_json::to_value(value).map_err(|_| SystemInputError::Serialization)?, + } + .try_into() + } + + pub fn schema(&self) -> &str { + &self.schema + } + + pub fn version(&self) -> u32 { + self.version + } + + /// The owner explicitly selects its contract before decoding. A valid unknown + /// payload can be retained and round-tripped without calling this operation. + pub fn decode( + &self, + schema: &str, + version: u32, + ) -> Result { + if self.schema != schema || self.version != version { + return Err(SystemInputError::UnsupportedPayload); + } + serde_json::from_value(self.value.clone()) + .map_err(|_| SystemInputError::InvalidTypedPayload) + } +} + +/// Eligibility is supplied independently for recording and Memory evidence. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum InputEvidenceUse { + Include, + Exclude, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum InputObservationUse { + Readable, + Hidden, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum InputCompactionUse { + /// Ordinary history eligible for the selected retained tail. + Retain, + /// A full opening summary consumed separately and superseded by compaction. + OpeningSummary, + /// Full historical context for the compactor, not an accumulating retained tail. + ReferenceInput, +} + +/// No permissive defaults: malformed/unknown policies cannot turn into eligibility. +/// Public presentation is deliberately not one of these use decisions. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SystemInputUsePolicy { + pub recording_material: InputEvidenceUse, + pub memory_evidence: InputEvidenceUse, + pub observation: InputObservationUse, + pub compaction: InputCompactionUse, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(try_from = "MetadataWire")] +pub struct SystemInputMetadata { + usage: SystemInputUsePolicy, + feature_payload: Option, + public: Option, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct MetadataWire { + usage: SystemInputUsePolicy, + feature_payload: Option, + public: Option, +} + +impl TryFrom for SystemInputMetadata { + type Error = SystemInputError; + + fn try_from(wire: MetadataWire) -> Result { + if let Some(public) = &wire.public { + // Kind is a descriptive label, not a Feature dispatch or authority key. + if public.kind.is_empty() + || public.kind.len() > MAX_SCHEMA_BYTES + || public.kind.chars().any(char::is_control) + { + return Err(SystemInputError::InvalidSchema); + } + let value = + serde_json::to_value(public).map_err(|_| SystemInputError::Serialization)?; + validate_json(&value, MAX_SYSTEM_PUBLIC_BYTES)?; + } + Ok(Self { + usage: wire.usage, + feature_payload: wire.feature_payload, + public: wire.public, + }) + } +} + +impl SystemInputMetadata { + pub fn new( + usage: SystemInputUsePolicy, + feature_payload: Option, + public: Option, + ) -> Result { + MetadataWire { + usage, + feature_payload, + public, + } + .try_into() + } + + pub fn usage(&self) -> SystemInputUsePolicy { + self.usage + } + + pub fn feature_payload(&self) -> Option<&FeatureInputPayload> { + self.feature_payload.as_ref() + } + + pub fn public_item(&self) -> Option<&PublicSystemItem> { + self.public.as_ref() + } +} + +/// System role is invariant; provenance and use policies are attached data. +/// An absent public projection never implicitly publishes the model body. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(try_from = "InputWire")] +pub struct SystemInputEnvelope { + body: String, + metadata: SystemInputMetadata, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct InputWire { + body: String, + metadata: SystemInputMetadata, +} + +impl TryFrom for SystemInputEnvelope { + type Error = SystemInputError; + + fn try_from(wire: InputWire) -> Result { + if wire.body.len() > MAX_SYSTEM_INPUT_BYTES { + return Err(SystemInputError::ResourceLimit); + } + Ok(Self { + body: wire.body, + metadata: wire.metadata, + }) + } +} + +impl SystemInputEnvelope { + pub fn new( + body: impl Into, + metadata: SystemInputMetadata, + ) -> Result { + InputWire { + body: body.into(), + metadata, + } + .try_into() + } + + pub fn body(&self) -> &str { + &self.body + } + + pub fn metadata(&self) -> &SystemInputMetadata { + &self.metadata + } + + pub fn to_history_item(&self) -> Item { + Item::system_message(self.body.clone()) + } + + pub fn into_parts(self) -> (String, SystemInputMetadata) { + (self.body, self.metadata) + } +} + +fn validate_json(value: &Value, max_bytes: usize) -> Result<(), SystemInputError> { + fn walk(value: &Value, depth: usize, remaining: &mut usize) -> Result<(), SystemInputError> { + if depth > MAX_JSON_DEPTH || *remaining == 0 { + return Err(SystemInputError::ResourceLimit); + } + *remaining -= 1; + match value { + Value::Array(values) => { + for value in values { + walk(value, depth + 1, remaining)?; + } + } + Value::Object(values) => { + for value in values.values() { + walk(value, depth + 1, remaining)?; + } + } + _ => {} + } + Ok(()) + } + let mut remaining = MAX_JSON_NODES; + walk(value, 0, &mut remaining)?; + let bytes = serde_json::to_vec(value).map_err(|_| SystemInputError::Serialization)?; + if bytes.len() > max_bytes { + return Err(SystemInputError::ResourceLimit); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{LoggedItem, LoggedRole, PublicSystemPresentation}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + #[serde(deny_unknown_fields)] + struct OwnerPayload { + source_digest: String, + omitted: bool, + } + + fn reference_policy() -> SystemInputUsePolicy { + SystemInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::ReferenceInput, + } + } + + #[test] + fn unknown_valid_payload_round_trips_without_domain_decode_or_public_exposure() { + let payload = FeatureInputPayload::encode( + "future-owner:context", + 99, + &OwnerPayload { + source_digest: "private-digest".into(), + omitted: true, + }, + ) + .unwrap(); + let envelope = SystemInputEnvelope::new( + "private model context", + SystemInputMetadata::new(reference_policy(), Some(payload), None).unwrap(), + ) + .unwrap(); + let restored: SystemInputEnvelope = + serde_json::from_slice(&serde_json::to_vec(&envelope).unwrap()).unwrap(); + assert_eq!(restored.body(), "private model context"); + assert!(restored.metadata().public_item().is_none()); + let saved_payload = restored.metadata().feature_payload().unwrap(); + assert_eq!(saved_payload.schema(), "future-owner:context"); + assert_eq!(saved_payload.version(), 99); + assert_eq!( + saved_payload.decode::("known-owner:context", 1), + Err(SystemInputError::UnsupportedPayload) + ); + assert_eq!( + serde_json::to_value(&restored).unwrap(), + serde_json::to_value(&envelope).unwrap() + ); + assert!(!format!("{saved_payload:?}").contains("private-digest")); + } + + #[test] + fn owner_decodes_typed_payload_only_for_its_explicit_contract() { + let input = OwnerPayload { + source_digest: "rev".into(), + omitted: false, + }; + let payload = FeatureInputPayload::encode("owner:context", 1, &input).unwrap(); + assert_eq!( + payload.decode::("owner:context", 1).unwrap(), + input + ); + let invalid = FeatureInputPayload::encode( + "owner:context", + 1, + &serde_json::json!({ "omitted": "not a bool" }), + ) + .unwrap(); + assert_eq!( + invalid.decode::("owner:context", 1), + Err(SystemInputError::InvalidTypedPayload) + ); + } + + #[test] + fn malformed_or_oversized_payload_is_rejected_at_construction_and_restore() { + for (schema, version, value) in [ + ("", 1, Value::Null), + ("invalid schema", 1, Value::Null), + ("owner:context", 0, Value::Null), + ( + "owner:context", + 1, + Value::String("x".repeat(MAX_FEATURE_PAYLOAD_BYTES)), + ), + ( + "owner:context", + 1, + Value::Array(vec![Value::Null; MAX_JSON_NODES]), + ), + ( + "owner:context", + 1, + (0..=MAX_JSON_DEPTH).fold(Value::Null, |inner, _| Value::Array(vec![inner])), + ), + ] { + assert!(FeatureInputPayload::encode(schema, version, &value).is_err()); + assert!( + serde_json::from_value::( + serde_json::json!({ "schema": schema, "version": version, "value": value }) + ) + .is_err() + ); + } + } + + #[test] + fn policy_is_required_and_unknown_or_malformed_fields_fail_closed() { + let valid = serde_json::to_value(reference_policy()).unwrap(); + for field in [ + "recording_material", + "memory_evidence", + "observation", + "compaction", + ] { + let mut absent = valid.clone(); + absent.as_object_mut().unwrap().remove(field); + assert!(serde_json::from_value::(absent).is_err()); + let mut unknown = valid.clone(); + unknown[field] = Value::String("future-allow".into()); + assert!(serde_json::from_value::(unknown).is_err()); + } + let mut extra = valid; + extra["feature_name"] = Value::String("infer-policy-from-me".into()); + assert!(serde_json::from_value::(extra).is_err()); + } + + #[test] + fn feature_name_and_model_body_do_not_select_policies_or_public_projection() { + for (schema, body) in [ + ("owner:journal", "past context"), + ("unrelated:skill", "do not extract this; legitimate wording"), + ] { + let public = PublicSystemItem { + kind: "context-label".into(), + body: "explicit public text".into(), + presentation: PublicSystemPresentation::Context, + }; + let metadata = SystemInputMetadata::new( + reference_policy(), + Some( + FeatureInputPayload::encode( + schema, + 1, + &OwnerPayload { + source_digest: "private".into(), + omitted: false, + }, + ) + .unwrap(), + ), + Some(public), + ) + .unwrap(); + let input = SystemInputEnvelope::new(body, metadata).unwrap(); + assert_eq!(input.metadata().usage(), reference_policy()); + let history: LoggedItem = input.to_history_item().into(); + assert!(matches!( + history, + LoggedItem::Message { + role: LoggedRole::System, + .. + } + )); + assert_eq!(input.to_history_item().as_text(), Some(body)); + let public = serde_json::to_string(input.metadata().public_item().unwrap()).unwrap(); + assert!(!public.contains("private")); + assert!(!public.contains(schema)); + assert!(!public.contains(body)); + } + } + + #[test] + fn use_policies_remain_independent_of_each_other_and_of_public_display() { + let usage = SystemInputUsePolicy { + recording_material: InputEvidenceUse::Include, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Readable, + compaction: InputCompactionUse::Retain, + }; + let metadata = SystemInputMetadata::new(usage, None, None).unwrap(); + let restored: SystemInputMetadata = + serde_json::from_value(serde_json::to_value(&metadata).unwrap()).unwrap(); + assert_eq!(restored.usage(), usage); + assert!(restored.public_item().is_none()); + assert!(restored.feature_payload().is_none()); + } + + #[test] + fn model_body_and_public_projection_have_independent_resource_limits() { + let metadata = SystemInputMetadata::new(reference_policy(), None, None).unwrap(); + assert!( + SystemInputEnvelope::new("x".repeat(MAX_SYSTEM_INPUT_BYTES + 1), metadata).is_err() + ); + let public = PublicSystemItem { + kind: "context".into(), + body: "x".repeat(MAX_SYSTEM_PUBLIC_BYTES), + presentation: PublicSystemPresentation::Context, + }; + assert!(SystemInputMetadata::new(reference_policy(), None, Some(public)).is_err()); + let invalid = serde_json::json!({ "body": "small", "metadata": { "public": null, "feature_payload": null } }); + assert!(serde_json::from_value::(invalid).is_err()); + } +} diff --git a/crates/session-store/src/system_item.rs b/crates/session-store/src/system_item.rs index be201e5cc..376a3cd8f 100644 --- a/crates/session-store/src/system_item.rs +++ b/crates/session-store/src/system_item.rs @@ -199,7 +199,7 @@ pub enum SystemItem { /// Allowlisted presentation data shared by live events and retained snapshots. /// Private Feature payloads and render provenance never cross this boundary. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct PublicSystemItem { pub kind: String, @@ -207,7 +207,7 @@ pub struct PublicSystemItem { pub presentation: PublicSystemPresentation, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum PublicSystemPresentation { Notification { message: String }, From bb5c50f9ab0318dc118ce9c3099b7559e0d0e512 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 02:44:28 +0900 Subject: [PATCH 05/11] feat: persist System input policies through public consumers --- crates/protocol/src/lib.rs | 14 ++ crates/session-store/src/fs_store.rs | 4 + crates/session-store/src/history.rs | 192 +++++++++++++++ .../session-store/src/legacy_session_log.rs | 1 + .../src/legacy_session_log/tests.rs | 1 + crates/session-store/src/public_index.rs | 2 + crates/session-store/src/public_material.rs | 119 +++++++-- .../src/public_material/tests.rs | 164 +++++++++++++ crates/session-store/src/public_snapshot.rs | 55 ++++- .../src/public_snapshot/run_stats.rs | 1 + crates/session-store/src/segment_log.rs | 18 ++ crates/session-store/src/store.rs | 3 + crates/session-store/src/system_input.rs | 21 ++ crates/session-store/src/system_item.rs | 11 +- .../session-store/src/worker_session_store.rs | 5 + crates/session-store/tests/fs_store_test.rs | 176 ++++++++++++++ crates/session-store/tests/session_test.rs | 1 + .../standalone/src/subjektiv/journal/tests.rs | 1 + crates/standalone/src/subjektiv_sessions.rs | 1 + crates/tui/src/app.rs | 2 + .../tests/session_material_tests.rs | 1 + crates/worker-runtime/src/retention.rs | 2 + crates/worker-runtime/src/runtime.rs | 1 + crates/worker-runtime/src/session_public.rs | 1 + crates/worker-runtime/src/worker_backend.rs | 1 + .../src/feature/builtin/session_explore.rs | 2 +- .../src/feature/builtin/worker_observation.rs | 1 + crates/worker/src/internal_worker.rs | 1 + crates/worker/src/ipc/protocol_session.rs | 3 +- crates/worker/src/session_capture.rs | 227 +++++++++++++++--- crates/worker/src/session_history.rs | 4 + crates/worker/src/spawn/comm_tools.rs | 1 + crates/worker/src/spawn/registry.rs | 1 + crates/worker/src/wip.rs | 1 + crates/worker/tests/compact_events_test.rs | 1 + crates/workspace-server/src/server.rs | 2 + .../server/tests/subjektiv_journal_tests.rs | 2 + .../tests/subjektiv_memory_journal_tests.rs | 1 + .../src/lib/generated/protocol-validator.ts | 2 +- web/workspace/src/lib/generated/protocol.ts | 2 +- 40 files changed, 988 insertions(+), 61 deletions(-) diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 8055193d5..8be8c3d9a 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -982,6 +982,17 @@ pub enum SessionEntryProvenance { LegacyUnknown, } +/// Public use decisions contain no Feature identity or private payload. Absence +/// denotes the older projection contract, not permission to expose System input. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct SessionEntryUsePolicy { + pub observation_readable: bool, + pub memory_evidence: bool, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] @@ -994,6 +1005,8 @@ pub struct SessionSnapshotEntry { pub provenance: SessionEntryProvenance, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub derived_from: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub use_policy: Option, #[serde(flatten)] pub data: SessionSnapshotEntryData, } @@ -2619,6 +2632,7 @@ mod tests { session: SessionSnapshot { pending_submissions: PendingSubmissionsSnapshot::default(), entries: vec![SessionSnapshotEntry { + use_policy: None, entry_id: "entry-1".into(), timestamp: 1, provenance: SessionEntryProvenance::HumanInput, diff --git a/crates/session-store/src/fs_store.rs b/crates/session-store/src/fs_store.rs index b6cade6b5..aa9988e8f 100644 --- a/crates/session-store/src/fs_store.rs +++ b/crates/session-store/src/fs_store.rs @@ -361,6 +361,7 @@ impl Store for FsStore { segment_id: SegmentId, entry: &LogEntry, ) -> Result<(), StoreError> { + entry.validate_system_inputs()?; let _retention = self.retention().lock_live(session_id)?; let line = serde_json::to_string(entry)?; self.append_line(&self.log_path(session_id, segment_id), &line) @@ -448,6 +449,9 @@ impl Store for FsStore { segment_id: SegmentId, entries: &[LogEntry], ) -> Result<(), StoreError> { + for entry in entries { + entry.validate_system_inputs()?; + } let _retention = self.retention().lock_live(session_id)?; let path = self.log_path(session_id, segment_id); if let Some(parent) = path.parent() { diff --git a/crates/session-store/src/history.rs b/crates/session-store/src/history.rs index 4997a3b83..f25617b1d 100644 --- a/crates/session-store/src/history.rs +++ b/crates/session-store/src/history.rs @@ -67,6 +67,10 @@ pub enum LoggedSessionHistoryOrigin { ToolOutput { worker: LoggedWorkerSubject, }, + /// Descriptive producer identity; use policies live in separate metadata. + Provided { + producer: String, + }, /// Host-produced opening context summary; not auto-read or TaskStore material. CompactionSummary, /// Frozen Host Journal reference, never independent observation evidence. @@ -86,14 +90,26 @@ pub struct LoggedSessionHistoryMetadata { pub origin: LoggedSessionHistoryOrigin, #[serde(default, skip_serializing_if = "Option::is_none")] pub derivation: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub system_input: Option, } impl LoggedSessionHistoryMetadata { + fn validate_system_input(&self) -> Result<(), crate::SystemInputError> { + if matches!(self.origin, LoggedSessionHistoryOrigin::Provided { .. }) + && self.system_input.is_none() + { + return Err(crate::SystemInputError::MissingMetadata); + } + Ok(()) + } + pub fn legacy_unknown() -> Self { Self { entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, + system_input: None, } } } @@ -101,20 +117,140 @@ impl LoggedSessionHistoryMetadata { /// Persisted item and metadata are one value so transforms cannot reorder or /// truncate one without the other. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "HistoryEntryWire")] pub struct LoggedHistoryEntry { pub item: LoggedItem, pub metadata: LoggedSessionHistoryMetadata, } +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct HistoryEntryWire { + item: LoggedItem, + metadata: LoggedSessionHistoryMetadata, +} + +impl TryFrom for LoggedHistoryEntry { + type Error = crate::SystemInputError; + + fn try_from(wire: HistoryEntryWire) -> Result { + let entry = Self { + item: wire.item, + metadata: wire.metadata, + }; + entry.validate_system_input()?; + Ok(entry) + } +} + +impl LoggedHistoryEntry { + pub fn validate_system_input(&self) -> Result<(), crate::SystemInputError> { + self.metadata.validate_system_input()?; + if self.metadata.system_input.is_some() { + let LoggedItem::Message { + role: crate::LoggedRole::System, + content, + } = &self.item + else { + return Err(crate::SystemInputError::InconsistentRecord); + }; + let [crate::LoggedContentPart::Text { text }] = content.as_slice() else { + return Err(crate::SystemInputError::InconsistentRecord); + }; + if text.len() > crate::system_input::MAX_SYSTEM_INPUT_BYTES { + return Err(crate::SystemInputError::ResourceLimit); + } + } + Ok(()) + } +} + /// Typed system-item history record. The typed system event remains available /// to client replay while its model-visible projection carries the same stable /// metadata used by live history. #[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(try_from = "SystemEntryWire")] pub struct LoggedSystemHistoryEntry { pub item: crate::SystemItem, pub metadata: LoggedSessionHistoryMetadata, } +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct SystemEntryWire { + item: crate::SystemItem, + metadata: LoggedSessionHistoryMetadata, +} + +impl TryFrom for LoggedSystemHistoryEntry { + type Error = crate::SystemInputError; + + fn try_from(wire: SystemEntryWire) -> Result { + let entry = Self { + item: wire.item, + metadata: wire.metadata, + }; + entry.validate_system_input()?; + Ok(entry) + } +} + +impl LoggedSystemHistoryEntry { + pub fn from_input( + origin: LoggedSessionHistoryOrigin, + derivation: Option, + input: crate::SystemInputEnvelope, + ) -> Self { + let (body, system_input) = input.into_parts(); + Self { + item: crate::SystemItem::Input { body }, + metadata: LoggedSessionHistoryMetadata { + entry_id: LoggedSessionHistoryEntryId::new(), + origin, + derivation, + system_input: Some(system_input), + }, + } + } + + pub fn validate_system_input(&self) -> Result<(), crate::SystemInputError> { + self.metadata.validate_system_input()?; + match (&self.item, &self.metadata.system_input) { + (crate::SystemItem::Input { body }, Some(_)) => { + if body.len() > crate::system_input::MAX_SYSTEM_INPUT_BYTES { + return Err(crate::SystemInputError::ResourceLimit); + } + } + (crate::SystemItem::Input { .. }, None) => { + return Err(crate::SystemInputError::MissingMetadata); + } + (_, Some(_)) => return Err(crate::SystemInputError::InconsistentRecord), + (_, None) => {} + } + Ok(()) + } + + pub fn to_history_entry(&self) -> LoggedHistoryEntry { + LoggedHistoryEntry { + item: self.item.to_history_item().into(), + metadata: self.metadata.clone(), + } + } + + pub fn public_item(&self) -> crate::PublicSystemItem { + if matches!(self.item, crate::SystemItem::Input { .. }) { + return self + .metadata + .system_input + .as_ref() + .and_then(crate::SystemInputMetadata::public_item) + .cloned() + .unwrap_or_else(|| self.item.public_item()); + } + self.item.public_item() + } +} + #[cfg(test)] mod tests { use super::*; @@ -132,6 +268,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".into(), @@ -150,6 +287,61 @@ mod tests { ); } + #[test] + fn external_system_records_reject_missing_policy_role_mismatch_and_overlimit_body() { + let valid = serde_json::json!({ + "item": { "kind": "input", "body": "context" }, + "metadata": { + "entry_id": "stable-entry", "origin": { "kind": "provided", "producer": "owner" }, + "system_input": { "usage": { + "recording_material": "exclude", "memory_evidence": "exclude", + "observation": "hidden", "compaction": "reference_input" + } } + } + }); + let restored: LoggedSystemHistoryEntry = serde_json::from_value(valid.clone()).unwrap(); + let flat = serde_json::to_value(restored.to_history_entry()).unwrap(); + assert!(serde_json::from_value::(flat.clone()).is_ok()); + for generic in [true, false] { + let wire = if generic { &valid } else { &flat }; + let decode = |wire| { + if generic { + serde_json::from_value::(wire).is_err() + } else { + serde_json::from_value::(wire).is_err() + } + }; + let mut missing = wire.clone(); + missing["metadata"] + .as_object_mut() + .unwrap() + .remove("system_input"); + assert!(decode(missing)); + let mut absent_policy = wire.clone(); + absent_policy["metadata"]["system_input"] + .as_object_mut() + .unwrap() + .remove("usage"); + assert!(decode(absent_policy)); + let mut invalid_role = wire.clone(); + invalid_role["item"] = + serde_json::to_value(LoggedItem::from(agen::Item::user_message("not system"))) + .unwrap(); + assert!(decode(invalid_role)); + let mut oversized = wire.clone(); + if generic { + oversized["item"]["body"] = "x" + .repeat(crate::system_input::MAX_SYSTEM_INPUT_BYTES + 1) + .into(); + } else { + oversized["item"]["content"][0]["text"] = "x" + .repeat(crate::system_input::MAX_SYSTEM_INPUT_BYTES + 1) + .into(); + } + assert!(decode(oversized)); + } + } + #[test] fn annotated_segment_start_is_restore_visible_without_projecting_metadata() { let session_id = uuid::Uuid::now_v7(); diff --git a/crates/session-store/src/legacy_session_log.rs b/crates/session-store/src/legacy_session_log.rs index 5c465d0b9..987a53984 100644 --- a/crates/session-store/src/legacy_session_log.rs +++ b/crates/session-store/src/legacy_session_log.rs @@ -167,6 +167,7 @@ fn legacy_metadata( identity.extend_from_slice(&(line_index as u64).to_be_bytes()); identity.extend_from_slice(&(item_index as u64).to_be_bytes()); LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId(format!( "l-{}", base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, identity) diff --git a/crates/session-store/src/legacy_session_log/tests.rs b/crates/session-store/src/legacy_session_log/tests.rs index 3c72462c5..314ee6d5f 100644 --- a/crates/session-store/src/legacy_session_log/tests.rs +++ b/crates/session-store/src/legacy_session_log/tests.rs @@ -22,6 +22,7 @@ fn seed( content: vec![crate::LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId("summary".into()), origin, derivation: Some(crate::LoggedHistoryDerivation { diff --git a/crates/session-store/src/public_index.rs b/crates/session-store/src/public_index.rs index 24932e397..cba29bc87 100644 --- a/crates/session-store/src/public_index.rs +++ b/crates/session-store/src/public_index.rs @@ -1296,6 +1296,7 @@ mod tests { LoggedHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId(id.to_owned()), origin, derivation: None, @@ -1537,6 +1538,7 @@ mod tests { prompt_provenance: None, }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId("system-item".into()), origin: LoggedSessionHistoryOrigin::BackendInstruction { operation_id: None, diff --git a/crates/session-store/src/public_material.rs b/crates/session-store/src/public_material.rs index 6487bdade..f27795928 100644 --- a/crates/session-store/src/public_material.rs +++ b/crates/session-store/src/public_material.rs @@ -148,15 +148,24 @@ struct SegmentHeader { file_len: u64, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PublicMaterialUse { + Recording, + MemoryEvidence, +} + /// A fixed material catalog. Opening a new catalog observes newly appended /// records; reading a body through this catalog verifies its saved digest and /// byte length again. Missing/corrected/deleted sources are explicit failures. +/// Explicit evidence permission applies to model text, not the private payload; +/// Web display and observation instead use the producer's public projection. pub struct SessionPublicMaterialReader { session_id: SessionId, active_segment_id: SegmentId, layout: Layout, limits: PublicMaterialLimits, materials: Vec, + purpose: PublicMaterialUse, retention: Option, } @@ -180,7 +189,13 @@ impl SessionPublicMaterialReader { active_segment_id: SegmentId, limits: PublicMaterialLimits, ) -> Result { - Self::open_worker_guarded(session_root, active_segment_id, limits, None) + Self::open_worker_guarded( + session_root, + active_segment_id, + limits, + None, + PublicMaterialUse::Recording, + ) } /// Uses the Host's aggregate-external control root for the whole scan and every body read. @@ -191,7 +206,30 @@ impl SessionPublicMaterialReader { limits: PublicMaterialLimits, retention: crate::retention::SessionRetentionStore, ) -> Result { - Self::open_worker_guarded(session_root, active_segment_id, limits, Some(retention)) + Self::open_worker_for_use( + session_root, + active_segment_id, + limits, + retention, + PublicMaterialUse::Recording, + ) + } + + /// Selects the evidence purpose independently of public display and observation. + pub fn open_worker_for_use( + session_root: &Path, + active_segment_id: SegmentId, + limits: PublicMaterialLimits, + retention: crate::retention::SessionRetentionStore, + purpose: PublicMaterialUse, + ) -> Result { + Self::open_worker_guarded( + session_root, + active_segment_id, + limits, + Some(retention), + purpose, + ) } fn open_worker_guarded( @@ -199,6 +237,7 @@ impl SessionPublicMaterialReader { active_segment_id: SegmentId, limits: PublicMaterialLimits, retention: Option, + purpose: PublicMaterialUse, ) -> Result { let store = WorkerSessionStore::open_read_only(session_root) .map_err(|_| PublicMaterialError::Unavailable)?; @@ -212,6 +251,7 @@ impl SessionPublicMaterialReader { Layout::Worker(session_root.into()), limits, retention, + purpose, ) } @@ -237,6 +277,25 @@ impl SessionPublicMaterialReader { active_segment_id: SegmentId, limits: PublicMaterialLimits, retention: crate::retention::SessionRetentionStore, + ) -> Result { + Self::open_fs_for_use( + sessions_root, + session_id, + active_segment_id, + limits, + retention, + PublicMaterialUse::Recording, + ) + } + + /// Uses retained originals with the independently selected evidence policy. + pub fn open_fs_for_use( + sessions_root: &Path, + session_id: SessionId, + active_segment_id: SegmentId, + limits: PublicMaterialLimits, + retention: crate::retention::SessionRetentionStore, + purpose: PublicMaterialUse, ) -> Result { Self::open( session_id, @@ -244,6 +303,7 @@ impl SessionPublicMaterialReader { Layout::Fs(sessions_root.join(session_id.to_string())), limits, Some(retention), + purpose, ) } @@ -263,6 +323,7 @@ impl SessionPublicMaterialReader { layout: Layout, limits: PublicMaterialLimits, retention: Option, + purpose: PublicMaterialUse, ) -> Result { let _guard = retention .as_ref() @@ -435,23 +496,19 @@ impl SessionPublicMaterialReader { } => turn = *total_turn_count, _ => {} } + let system_history; let entries: &[LoggedHistoryEntry] = match &record { LogEntry::AnnotatedSegmentStart { history, .. } | LogEntry::AnnotatedUserInput { history, .. } => history, LogEntry::AnnotatedAssistantItem { entry, .. } | LogEntry::AnnotatedToolResult { entry, .. } | LogEntry::ToolResultCorrected { entry, .. } => std::slice::from_ref(entry), + LogEntry::AnnotatedSystemItem { entry, .. } => { + system_history = entry.to_history_entry(); + std::slice::from_ref(&system_history) + } _ => &[], }; - if let LogEntry::AnnotatedSystemItem { entry, .. } = &record { - charge(&mut examined, limits.max_entries)?; - if entry.metadata.entry_id.0.is_empty() - || !history_seen.insert(entry.metadata.entry_id.0.clone()) - { - return Err(PublicMaterialError::Inconsistent); - } - history_ids.push(entry.metadata.entry_id.0.clone()); - } for (history_index, entry) in entries.iter().enumerate() { charge(&mut examined, limits.max_entries)?; let id = &entry.metadata.entry_id.0; @@ -468,7 +525,7 @@ impl SessionPublicMaterialReader { if in_run && ai_materialized(&entry.item) { safe = true; } - let Some((sha256, total_bytes)) = canonical_metadata(entry)? else { + let Some((sha256, total_bytes)) = canonical_metadata(entry, purpose)? else { continue; }; let location = Location { @@ -621,6 +678,7 @@ impl SessionPublicMaterialReader { layout, limits, materials, + purpose, retention: retention.clone(), }) } @@ -660,6 +718,7 @@ impl SessionPublicMaterialReader { let Some((_, record)) = reader.next()? else { return Err(PublicMaterialError::Missing); }; + let system_history; let (body, origin) = if let Some(boundary) = run_boundary(&record) { if boundary_reference( &record, @@ -683,12 +742,16 @@ impl SessionPublicMaterialReader { LogEntry::AnnotatedAssistantItem { entry, .. } | LogEntry::AnnotatedToolResult { entry, .. } | LogEntry::ToolResultCorrected { entry, .. } => Some(entry), + LogEntry::AnnotatedSystemItem { entry, .. } => { + system_history = entry.to_history_entry(); + Some(&system_history) + } _ => None, } .filter(|e| format!("E{}", e.metadata.entry_id.0) == descriptor.identity.entry_ref) .ok_or(PublicMaterialError::Changed)?; ( - canonical_body(entry).ok_or(PublicMaterialError::Changed)?, + canonical_body(entry, self.purpose).ok_or(PublicMaterialError::Changed)?, PublicMaterialOrigin::History { origin: entry.metadata.origin.clone(), }, @@ -828,7 +891,32 @@ fn boundary_reference( .unwrap_or_else(|| format!("El-run-{session}-{segment}-{offset}")) } -fn canonical_body(entry: &LoggedHistoryEntry) -> Option> { +fn canonical_body( + entry: &LoggedHistoryEntry, + purpose: PublicMaterialUse, +) -> Option> { + if let Some(input) = &entry.metadata.system_input { + let usage = input.usage(); + let allowed = match purpose { + PublicMaterialUse::Recording => usage.recording_material, + PublicMaterialUse::MemoryEvidence => usage.memory_evidence, + }; + if allowed == crate::InputEvidenceUse::Exclude { + return None; + } + if let LoggedItem::Message { + role: LoggedRole::System, + content, + } = &entry.item + { + return Some(CanonicalBody::Message { + origin: &entry.metadata.origin, + role: LoggedRole::System, + content, + }); + } + return None; + } if matches!( entry.metadata.origin, LoggedSessionHistoryOrigin::DerivedSummary @@ -880,8 +968,9 @@ fn canonical_body(entry: &LoggedHistoryEntry) -> Option> { fn canonical_metadata( entry: &LoggedHistoryEntry, + purpose: PublicMaterialUse, ) -> Result, PublicMaterialError> { - let Some(body) = canonical_body(entry) else { + let Some(body) = canonical_body(entry, purpose) else { return Ok(None); }; let mut sink = BodySink::new(0, 0); diff --git a/crates/session-store/src/public_material/tests.rs b/crates/session-store/src/public_material/tests.rs index b77642840..0376af0bc 100644 --- a/crates/session-store/src/public_material/tests.rs +++ b/crates/session-store/src/public_material/tests.rs @@ -9,6 +9,7 @@ fn entry(id: &str, item: LoggedItem) -> LoggedHistoryEntry { LoggedHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId(id.into()), origin: LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, @@ -591,6 +592,7 @@ fn reasoning_proves_rollback_safety_but_private_and_derived_items_are_not_materi }, }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId("startup-journal".into()), origin: LoggedSessionHistoryOrigin::DerivedSummary, derivation: None, @@ -1243,3 +1245,165 @@ fn compaction_without_exact_derivation_or_with_invalid_turn_is_rejected() { )); } } + +#[test] +fn generic_system_material_uses_explicit_evidence_purpose_not_producer_or_body() { + use crate::{ + FeatureInputPayload, InputCompactionUse, InputEvidenceUse, InputObservationUse, + LoggedSystemHistoryEntry, SystemInputEnvelope, SystemInputMetadata, SystemInputUsePolicy, + }; + for worker_layout in [false, true] { + let temp = tempfile::tempdir().unwrap(); + let session = crate::new_session_id(); + let segment = crate::new_segment_id(); + let fork = crate::new_segment_id(); + let make = |producer: &str, recording_material, memory_evidence, body: &str| { + LoggedSystemHistoryEntry::from_input( + LoggedSessionHistoryOrigin::Provided { + producer: producer.into(), + }, + None, + SystemInputEnvelope::new( + body, + SystemInputMetadata::new( + SystemInputUsePolicy { + recording_material, + memory_evidence, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::Retain, + }, + Some( + FeatureInputPayload::encode( + "unknown:future", + 99, + &serde_json::json!({"private": "not-public"}), + ) + .unwrap(), + ), + None, + ) + .unwrap(), + ) + .unwrap(), + ) + }; + let inputs = [ + make( + "same-owner", + InputEvidenceUse::Include, + InputEvidenceUse::Exclude, + "do not extract this; original material", + ), + make( + "same-owner", + InputEvidenceUse::Exclude, + InputEvidenceUse::Include, + "memory-only original", + ), + make( + "renamed-owner", + InputEvidenceUse::Exclude, + InputEvidenceUse::Exclude, + "reference without warning text", + ), + ]; + let root = if worker_layout { + temp.path().join("worker") + } else { + temp.path().join(session.to_string()) + }; + let store: Box = if worker_layout { + Box::new(WorkerSessionStore::new(&root).unwrap()) + } else { + Box::new(FsStore::new(temp.path()).unwrap()) + }; + store + .create_segment(session, segment, &[seed(session, vec![])]) + .unwrap(); + for entry in &inputs { + store + .append( + session, + segment, + &LogEntry::AnnotatedSystemItem { + ts: 2, + entry: entry.clone(), + extensions: vec![], + }, + ) + .unwrap(); + } + store + .append( + session, + segment, + &LogEntry::TurnEnd { + ts: 3, + turn_count: 1, + }, + ) + .unwrap(); + let mut fork_seed = seed( + session, + inputs + .iter() + .map(LoggedSystemHistoryEntry::to_history_entry) + .collect(), + ); + if let LogEntry::AnnotatedSegmentStart { forked_from, .. } = &mut fork_seed { + *forked_from = Some(crate::SegmentOrigin { + segment_id: segment, + at_turn_index: 1, + }); + } + store.create_segment(session, fork, &[fork_seed]).unwrap(); + let open = |active, purpose| { + if worker_layout { + SessionPublicMaterialReader::open_worker_for_use( + &root, + active, + Default::default(), + crate::retention::fs_retention(temp.path()), + purpose, + ) + .unwrap() + } else { + SessionPublicMaterialReader::open_fs_for_use( + temp.path(), + session, + active, + Default::default(), + crate::retention::fs_retention(temp.path()), + purpose, + ) + .unwrap() + } + }; + for (purpose, selected, expected_text) in [ + ( + PublicMaterialUse::Recording, + 0, + "do not extract this; original material", + ), + (PublicMaterialUse::MemoryEvidence, 1, "memory-only original"), + ] { + let original = open(segment, purpose); + let inherited = open(fork, purpose); + assert_eq!( + ids(&original), + vec![format!("E{}", inputs[selected].metadata.entry_id.0)] + ); + assert_eq!(ids(&inherited), ids(&original)); + let descriptor = inherited.descriptors().next().unwrap(); + let body = inherited + .read_content(descriptor, 0, 16 * 1024) + .unwrap() + .content; + let value: serde_json::Value = serde_json::from_str(&body).unwrap(); + assert_eq!(value["role"], "system"); + assert_eq!(value["content"][0]["text"], expected_text); + assert!(!body.contains("not-public")); + assert!(!body.contains("unknown:future")); + } + } +} diff --git a/crates/session-store/src/public_snapshot.rs b/crates/session-store/src/public_snapshot.rs index 6a1d94b0e..1baa121c8 100644 --- a/crates/session-store/src/public_snapshot.rs +++ b/crates/session-store/src/public_snapshot.rs @@ -27,7 +27,7 @@ use thiserror::Error; use crate::{ LogEntry, LoggedContentPart, LoggedHistoryEntry, LoggedItem, LoggedRole, - LoggedSessionHistoryOrigin, SegmentId, SegmentOrigin, SessionId, StoreError, SystemItem, + LoggedSessionHistoryOrigin, SegmentId, SegmentOrigin, SessionId, StoreError, WorkerAggregateStore, WorkerSessionStore, WorkerStoreError, }; @@ -1165,7 +1165,7 @@ fn capture_tool_correction( let LogEntry::ToolResultCorrected { entry, .. } = record else { return false; }; - if let Some(data) = project_item(&entry.metadata.entry_id.0, &entry.item) { + if let Some(data) = project_history_item(entry) { // We read backwards: the first correction seen is the latest one. corrections .entry(entry.metadata.entry_id.0.clone()) @@ -1600,12 +1600,12 @@ pub(crate) fn project_session_snapshot_for_segment( } => extend_history(&mut entries, history, Some(segments), *ts), LogEntry::AnnotatedAssistantItem { ts, entry } | LogEntry::AnnotatedToolResult { ts, entry } => { - if let Some(data) = project_item(&entry.metadata.entry_id.0, &entry.item) { + if let Some(data) = project_history_item(entry) { entries.push(history_entry(entry, *ts, data)); } } LogEntry::ToolResultCorrected { ts, entry } => { - if let Some(data) = project_item(&entry.metadata.entry_id.0, &entry.item) { + if let Some(data) = project_history_item(entry) { if let Some(existing) = entries .iter_mut() .find(|existing| existing.entry_id == entry.metadata.entry_id.0) @@ -1618,11 +1618,16 @@ pub(crate) fn project_session_snapshot_for_segment( } } LogEntry::AnnotatedSystemItem { ts, entry, .. } => entries.push(system_entry( - &entry.item, + &entry.public_item(), entry.metadata.entry_id.0.clone(), *ts, provenance(&entry.metadata.origin), derivation_ids(entry), + entry + .metadata + .system_input + .as_ref() + .map(crate::SystemInputMetadata::public_use_policy), )), LogEntry::RunYielded { ts, @@ -1775,7 +1780,7 @@ fn extend_history( segments: input_segments.cloned().unwrap_or_default(), } } else { - let Some(data) = project_item(&entry.metadata.entry_id.0, &entry.item) else { + let Some(data) = project_history_item(entry) else { continue; }; data @@ -1790,6 +1795,11 @@ fn history_entry( data: SessionSnapshotEntryData, ) -> SessionSnapshotEntry { SessionSnapshotEntry { + use_policy: entry + .metadata + .system_input + .as_ref() + .map(crate::SystemInputMetadata::public_use_policy), entry_id: entry.metadata.entry_id.0.clone(), timestamp, provenance: provenance(&entry.metadata.origin), @@ -1833,6 +1843,7 @@ fn run_transition_entry( data: SessionSnapshotEntryData, ) -> SessionSnapshotEntry { SessionSnapshotEntry { + use_policy: None, entry_id: persisted_entry_id .map(|entry_id| entry_id.0.clone()) .unwrap_or_else(|| legacy_entry_id(session_key, segment_id, log_index, 0)), @@ -1852,6 +1863,7 @@ fn legacy_entry( data: SessionSnapshotEntryData, ) -> SessionSnapshotEntry { SessionSnapshotEntry { + use_policy: None, entry_id: legacy_entry_id(session_key, segment_id, log_index, item_index), timestamp, provenance: SessionEntryProvenance::LegacyUnknown, @@ -1884,9 +1896,8 @@ fn provenance(origin: &LoggedSessionHistoryOrigin) -> SessionEntryProvenance { LoggedSessionHistoryOrigin::FlowInstruction { .. } => { SessionEntryProvenance::FlowInstruction } - LoggedSessionHistoryOrigin::BackendInstruction { .. } => { - SessionEntryProvenance::BackendInstruction - } + LoggedSessionHistoryOrigin::BackendInstruction { .. } + | LoggedSessionHistoryOrigin::Provided { .. } => SessionEntryProvenance::BackendInstruction, LoggedSessionHistoryOrigin::ModelOutput { .. } => SessionEntryProvenance::ModelOutput, LoggedSessionHistoryOrigin::ToolOutput { .. } => SessionEntryProvenance::ToolOutput, LoggedSessionHistoryOrigin::CompactionSummary @@ -1914,6 +1925,18 @@ pub fn session_tool_attachment_id( URL_SAFE_NO_PAD.encode(hash.finalize()) } +fn project_history_item(entry: &LoggedHistoryEntry) -> Option { + if let Some(input) = &entry.metadata.system_input { + let public = input.public_projection(); + return Some(SessionSnapshotEntryData::SystemItem { + item_kind: public.kind.clone(), + content: public.body.clone(), + data: Some(serde_json::to_value(public).expect("public system input is serializable")), + }); + } + project_item(&entry.metadata.entry_id.0, &entry.item) +} + fn project_item(entry_id: &str, item: &LoggedItem) -> Option { match item { LoggedItem::Message { role, content } => { @@ -1986,17 +2009,19 @@ fn project_item(entry_id: &str, item: &LoggedItem) -> Option, + use_policy: Option, ) -> SessionSnapshotEntry { - let public = item.public_item(); + let public = item.clone(); let data = Some(serde_json::to_value(&public).expect("public system input is serializable")); let item_kind = public.kind; let content = public.body; SessionSnapshotEntry { + use_policy, entry_id, timestamp, provenance, @@ -2285,6 +2310,7 @@ mod tests { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId(entry_id.into()), origin, derivation: None, @@ -3583,6 +3609,7 @@ mod tests { .map(|item| LoggedHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -3637,6 +3664,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: user_entry_id, origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".into(), @@ -3706,6 +3734,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::FlowInstruction { selector: "builtin:coder-review".into(), @@ -3725,6 +3754,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: user_entry_id.clone(), origin: origin.clone(), derivation: Some(LoggedHistoryDerivation { @@ -3755,6 +3785,7 @@ mod tests { fn annotated_projection_preserves_identity_and_provenance() { let session_id = crate::new_session_id(); let metadata = LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::ModelOutput { worker: LoggedWorkerSubject { @@ -4317,6 +4348,7 @@ mod tests { is_error: false, }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId("ancestor-image".into()), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -4411,6 +4443,7 @@ mod tests { is_error: false, }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId("tool-images".into()), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/session-store/src/public_snapshot/run_stats.rs b/crates/session-store/src/public_snapshot/run_stats.rs index 2d5b49902..4806e1da2 100644 --- a/crates/session-store/src/public_snapshot/run_stats.rs +++ b/crates/session-store/src/public_snapshot/run_stats.rs @@ -103,6 +103,7 @@ fn finish( stats.insert( anchor.clone(), SessionSnapshotEntry { + use_policy: None, entry_id: format!("run-stats:{anchor}"), timestamp: ended_at, provenance: SessionEntryProvenance::LegacyUnknown, diff --git a/crates/session-store/src/segment_log.rs b/crates/session-store/src/segment_log.rs index 97f347dad..f76ea783c 100644 --- a/crates/session-store/src/segment_log.rs +++ b/crates/session-store/src/segment_log.rs @@ -254,6 +254,23 @@ pub struct SegmentOrigin { } impl LogEntry { + /// Validate generic System records before a Store publishes any bytes. + pub fn validate_system_inputs(&self) -> Result<(), crate::SystemInputError> { + match self { + Self::AnnotatedSegmentStart { history, .. } + | Self::AnnotatedUserInput { history, .. } => { + for entry in history { + entry.validate_system_input()?; + } + } + Self::AnnotatedAssistantItem { entry, .. } + | Self::AnnotatedToolResult { entry, .. } + | Self::ToolResultCorrected { entry, .. } => entry.validate_system_input()?, + Self::AnnotatedSystemItem { entry, .. } => entry.validate_system_input()?, + _ => {} + } + Ok(()) + } /// Allocate the durable public identity for a newly committed Run /// transition. Calling this repeatedly is idempotent so the exact value /// written to storage is also the value published to live subscribers. @@ -537,6 +554,7 @@ mod tests { LoggedHistoryEntry { item: LoggedItem::from(item), metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/session-store/src/store.rs b/crates/session-store/src/store.rs index a876b7429..bf29ccb63 100644 --- a/crates/session-store/src/store.rs +++ b/crates/session-store/src/store.rs @@ -27,6 +27,9 @@ pub enum StoreError { #[error("serialization error: {0}")] Serde(#[from] serde_json::Error), + #[error("invalid system input: {0}")] + InvalidSystemInput(#[from] crate::SystemInputError), + #[error("segment not found: {0}")] NotFound(SegmentId), diff --git a/crates/session-store/src/system_input.rs b/crates/session-store/src/system_input.rs index fc9613941..3595c7bdb 100644 --- a/crates/session-store/src/system_input.rs +++ b/crates/session-store/src/system_input.rs @@ -36,6 +36,10 @@ pub enum SystemInputError { UnsupportedPayload, #[error("system input payload does not match the owner's typed contract")] InvalidTypedPayload, + #[error("generic system input is missing explicit metadata")] + MissingMetadata, + #[error("system input metadata does not match its logical record")] + InconsistentRecord, } /// One private payload owned by a Feature's versioned typed decoder. @@ -229,6 +233,23 @@ impl SystemInputMetadata { pub fn public_item(&self) -> Option<&PublicSystemItem> { self.public.as_ref() } + + pub fn public_use_policy(&self) -> protocol::SessionEntryUsePolicy { + let has_public = self.public.is_some(); + protocol::SessionEntryUsePolicy { + observation_readable: has_public + && self.usage.observation == InputObservationUse::Readable, + memory_evidence: has_public && self.usage.memory_evidence == InputEvidenceUse::Include, + } + } + + pub fn public_projection(&self) -> PublicSystemItem { + self.public.clone().unwrap_or_else(|| PublicSystemItem { + kind: "input".into(), + body: String::new(), + presentation: crate::PublicSystemPresentation::Ignored, + }) + } } /// System role is invariant; provenance and use policies are attached data. diff --git a/crates/session-store/src/system_item.rs b/crates/session-store/src/system_item.rs index 376a3cd8f..6d3d6ce43 100644 --- a/crates/session-store/src/system_item.rs +++ b/crates/session-store/src/system_item.rs @@ -92,6 +92,9 @@ pub struct PromptRenderProvenance { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum SystemItem { + /// Generic System body; private metadata and public presentation are supplied + /// independently by the producer in the same annotated history record. + Input { body: String }, /// One-time derived Journal reference context. The bounded Host snapshot is /// retained as JSON independently of its rendered body and prompt projection. JournalStartup { @@ -227,9 +230,9 @@ impl SystemItem { event: event.clone(), }, Self::TaskReminder { .. } => PublicSystemPresentation::TaskReminder, - Self::LegacyKnowledgeIgnored { .. } | Self::LegacyIgnored { .. } => { - PublicSystemPresentation::Ignored - } + Self::Input { .. } + | Self::LegacyKnowledgeIgnored { .. } + | Self::LegacyIgnored { .. } => PublicSystemPresentation::Ignored, Self::JournalStartup { .. } | Self::ResidentSummaryRefresh { .. } | Self::SubjectBehaviorRefresh { .. } @@ -253,6 +256,7 @@ impl SystemItem { /// for this item. Returns the variant's stored `body` verbatim. pub fn history_text(&self) -> String { match self { + SystemItem::Input { body } => body.clone(), SystemItem::JournalStartup { body, .. } => body.clone(), SystemItem::Notification { body, .. } => body.clone(), SystemItem::WorkerEvent { body, .. } => body.clone(), @@ -280,6 +284,7 @@ impl SystemItem { /// wire — keep flexible. pub fn kind_label(&self) -> &'static str { match self { + SystemItem::Input { .. } => "input", SystemItem::JournalStartup { .. } => "journal_startup", SystemItem::Notification { .. } => "notification", SystemItem::WorkerEvent { .. } => "worker_event", diff --git a/crates/session-store/src/worker_session_store.rs b/crates/session-store/src/worker_session_store.rs index b19180b3f..bb6ffe34f 100644 --- a/crates/session-store/src/worker_session_store.rs +++ b/crates/session-store/src/worker_session_store.rs @@ -742,6 +742,7 @@ impl Store for WorkerSessionStore { segment_id: SegmentId, entry: &LogEntry, ) -> Result<(), StoreError> { + entry.validate_system_inputs()?; let _retention = self.lock_publication(session_id)?; self.ensure_session(session_id, true)?; self.append_log_entry(&self.log_path(segment_id), entry) @@ -800,6 +801,9 @@ impl Store for WorkerSessionStore { segment_id: SegmentId, entries: &[LogEntry], ) -> Result<(), StoreError> { + for entry in entries { + entry.validate_system_inputs()?; + } let _retention = self.lock_publication(session_id)?; self.ensure_session(session_id, true)?; let mut content = Vec::new(); @@ -1430,6 +1434,7 @@ mod tests { LoggedHistoryEntry { item: LoggedItem::from(item), metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/session-store/tests/fs_store_test.rs b/crates/session-store/tests/fs_store_test.rs index 1ecb81d01..af4e940ec 100644 --- a/crates/session-store/tests/fs_store_test.rs +++ b/crates/session-store/tests/fs_store_test.rs @@ -11,6 +11,7 @@ fn annotated(item: Item) -> LoggedHistoryEntry { LoggedHistoryEntry { item: LoggedItem::from(item), metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -421,3 +422,178 @@ fn lookup_session_of_finds_owning_session() { assert_eq!(store.lookup_session_of(segid).unwrap(), Some(sid)); } + +// Storage/projection component test: both production layouts, temporary originals, +// no Feature, model, runtime or Host authority is substituted into this boundary. +#[test] +fn generic_system_append_and_fork_preserve_metadata_without_public_payload_leak() { + use session_store::{ + FeatureInputPayload, InputCompactionUse, InputEvidenceUse, InputObservationUse, + LoggedHistoryDerivation, LoggedSystemHistoryEntry, PublicSystemItem, + PublicSystemPresentation, SystemInputEnvelope, SystemInputMetadata, SystemInputUsePolicy, + WorkerSessionStore, + }; + for worker_layout in [false, true] { + let dir = tempfile::tempdir().unwrap(); + let sid = new_session_id(); + let segment = new_segment_id(); + let fork = new_segment_id(); + let store: Box = if worker_layout { + Box::new(WorkerSessionStore::new(dir.path()).unwrap()) + } else { + Box::new(FsStore::new(dir.path()).unwrap()) + }; + store + .create_segment(sid, segment, &[nil_session_start(1, sid)]) + .unwrap(); + let entry = LoggedSystemHistoryEntry::from_input( + LoggedSessionHistoryOrigin::Provided { + producer: "new-feature".into(), + }, + Some(LoggedHistoryDerivation { + sources: vec![LoggedSessionHistoryEntryId("source-entry".into())], + }), + SystemInputEnvelope::new( + "private model body", + SystemInputMetadata::new( + SystemInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::ReferenceInput, + }, + Some( + FeatureInputPayload::encode( + "new-feature:context", + 101, + &serde_json::json!({"private": "secret-detail"}), + ) + .unwrap(), + ), + Some(PublicSystemItem { + kind: "context".into(), + body: "explicit public body".into(), + presentation: PublicSystemPresentation::Context, + }), + ) + .unwrap(), + ) + .unwrap(), + ); + store + .append( + sid, + segment, + &LogEntry::AnnotatedSystemItem { + ts: 2, + entry: entry.clone(), + extensions: vec![], + }, + ) + .unwrap(); + let saved = store.read_all(sid, segment).unwrap(); + let LogEntry::AnnotatedSystemItem { + entry: restored, .. + } = &saved[1] + else { + panic!("missing system input") + }; + assert_eq!(restored.to_history_entry(), entry.to_history_entry()); + assert_eq!( + collect_state(&saved).history[0].as_text(), + Some("private model body") + ); + let mut fork_seed = nil_session_start(3, sid); + if let LogEntry::AnnotatedSegmentStart { + history, + forked_from, + .. + } = &mut fork_seed + { + history.push(restored.to_history_entry()); + *forked_from = Some(session_store::SegmentOrigin { + segment_id: segment, + at_turn_index: 0, + }); + } + store.create_segment(sid, fork, &[fork_seed]).unwrap(); + let fork_saved = store.read_all(sid, fork).unwrap(); + let LogEntry::AnnotatedSegmentStart { history, .. } = &fork_saved[0] else { + unreachable!() + }; + assert_eq!(history, &[entry.to_history_entry()]); + let original = session_store::public_snapshot::project_session_snapshot(sid, &saved); + let inherited = session_store::public_snapshot::project_session_snapshot(sid, &fork_saved); + let public = serde_json::to_string(&original.entries).unwrap(); + assert!(public.contains("explicit public body")); + for private in ["private model body", "secret-detail", "new-feature:context"] { + assert!(!public.contains(private), "private field leaked: {private}"); + } + assert_eq!(original.entries.len(), 1); + assert_eq!(inherited.entries.len(), 1); + assert_eq!(original.entries[0].entry_id, inherited.entries[0].entry_id); + assert_eq!( + original.entries[0].derived_from, + inherited.entries[0].derived_from + ); + assert_eq!( + original.entries[0].provenance, + inherited.entries[0].provenance + ); + assert_eq!( + serde_json::to_value(&original.entries[0].data).unwrap(), + serde_json::to_value(&inherited.entries[0].data).unwrap() + ); + } +} + +#[test] +fn invalid_generic_system_input_rejects_append_create_and_external_json_without_changing_originals() +{ + use session_store::{StoreError, SystemItem, WorkerSessionStore}; + for worker_layout in [false, true] { + let dir = tempfile::tempdir().unwrap(); + let sid = new_session_id(); + let segment = new_segment_id(); + let store: Box = if worker_layout { + Box::new(WorkerSessionStore::new(dir.path()).unwrap()) + } else { + Box::new(FsStore::new(dir.path()).unwrap()) + }; + let seed = nil_session_start(1, sid); + store + .create_segment(sid, segment, std::slice::from_ref(&seed)) + .unwrap(); + let path = if worker_layout { + dir.path().join("segments").join(format!("{segment}.jsonl")) + } else { + dir.path() + .join(sid.to_string()) + .join(format!("{segment}.jsonl")) + }; + let before = std::fs::read(&path).unwrap(); + let invalid = LogEntry::AnnotatedSystemItem { + ts: 2, + extensions: vec![], + entry: session_store::LoggedSystemHistoryEntry { + item: SystemItem::Input { + body: "no policy".into(), + }, + metadata: LoggedSessionHistoryMetadata::legacy_unknown(), + }, + }; + assert!(matches!( + store.append(sid, segment, &invalid), + Err(StoreError::InvalidSystemInput(_)) + )); + assert!(matches!( + store.create_segment(sid, segment, &[seed, invalid.clone()]), + Err(StoreError::InvalidSystemInput(_)) + )); + assert!( + serde_json::from_value::(serde_json::to_value(invalid).unwrap()).is_err() + ); + assert_eq!(std::fs::read(path).unwrap(), before); + assert_eq!(store.read_all(sid, segment).unwrap().len(), 1); + } +} diff --git a/crates/session-store/tests/session_test.rs b/crates/session-store/tests/session_test.rs index 901c0578f..e12d3ff29 100644 --- a/crates/session-store/tests/session_test.rs +++ b/crates/session-store/tests/session_test.rs @@ -24,6 +24,7 @@ fn annotated(items: &[Item]) -> Vec { .map(|item| session_store::LoggedHistoryEntry { item: session_store::LoggedItem::from(item), metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId::new(), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/standalone/src/subjektiv/journal/tests.rs b/crates/standalone/src/subjektiv/journal/tests.rs index 3eacfeeb0..bd24ef67d 100644 --- a/crates/standalone/src/subjektiv/journal/tests.rs +++ b/crates/standalone/src/subjektiv/journal/tests.rs @@ -212,6 +212,7 @@ fn message(id: &str, text: &str) -> session_store::LoggedHistoryEntry { content: vec![session_store::LoggedContentPart::Text { text: text.into() }], }, metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId(id.into()), origin: session_store::LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, diff --git a/crates/standalone/src/subjektiv_sessions.rs b/crates/standalone/src/subjektiv_sessions.rs index 36a75df06..71dc48631 100644 --- a/crates/standalone/src/subjektiv_sessions.rs +++ b/crates/standalone/src/subjektiv_sessions.rs @@ -1621,6 +1621,7 @@ allow = [] let history = |id: &str, item: LoggedItem| LoggedHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId(id.into()), origin: LoggedSessionHistoryOrigin::BackendInstruction { operation_id: None }, derivation: None, diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 306368070..e537c5385 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -3567,6 +3567,7 @@ mod completion_flow_tests { session_store::LoggedHistoryEntry { item: session_store::LoggedItem::from(item), metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId::new(), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -4324,6 +4325,7 @@ mod completion_flow_tests { })); app.handle_worker_event(Event::SessionEntryCommitted { entry: protocol::SessionSnapshotEntry { + use_policy: None, entry_id: "run-failure".into(), timestamp: 2, provenance: protocol::SessionEntryProvenance::LegacyUnknown, diff --git a/crates/worker-runtime/src/http_server/tests/session_material_tests.rs b/crates/worker-runtime/src/http_server/tests/session_material_tests.rs index 33f8555dc..9c0e5f9e2 100644 --- a/crates/worker-runtime/src/http_server/tests/session_material_tests.rs +++ b/crates/worker-runtime/src/http_server/tests/session_material_tests.rs @@ -35,6 +35,7 @@ fn message(id: &str, role: LoggedRole, text: &str) -> LoggedHistoryEntry { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId(id.into()), origin: LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, diff --git a/crates/worker-runtime/src/retention.rs b/crates/worker-runtime/src/retention.rs index bc7994de9..9777e7390 100644 --- a/crates/worker-runtime/src/retention.rs +++ b/crates/worker-runtime/src/retention.rs @@ -1831,6 +1831,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId("original".into()), origin: LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, @@ -1844,6 +1845,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId("summary".into()), origin: LoggedSessionHistoryOrigin::DerivedSummary, derivation: Some(session_store::LoggedHistoryDerivation { diff --git a/crates/worker-runtime/src/runtime.rs b/crates/worker-runtime/src/runtime.rs index 2b671c6e5..c9307a5dc 100644 --- a/crates/worker-runtime/src/runtime.rs +++ b/crates/worker-runtime/src/runtime.rs @@ -12422,6 +12422,7 @@ mod tests { session: protocol::SessionSnapshot { pending_submissions: protocol::PendingSubmissionsSnapshot::default(), entries: vec![protocol::SessionSnapshotEntry { + use_policy: None, entry_id: "restored-log-entry".to_owned(), timestamp: 1, provenance: protocol::SessionEntryProvenance::LegacyUnknown, diff --git a/crates/worker-runtime/src/session_public.rs b/crates/worker-runtime/src/session_public.rs index dac5d21f0..93f337f6d 100644 --- a/crates/worker-runtime/src/session_public.rs +++ b/crates/worker-runtime/src/session_public.rs @@ -398,6 +398,7 @@ mod tests { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId(id.to_string()), origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".to_string(), diff --git a/crates/worker-runtime/src/worker_backend.rs b/crates/worker-runtime/src/worker_backend.rs index 74044cde9..ca598cd80 100644 --- a/crates/worker-runtime/src/worker_backend.rs +++ b/crates/worker-runtime/src/worker_backend.rs @@ -5033,6 +5033,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id, origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/worker/src/feature/builtin/session_explore.rs b/crates/worker/src/feature/builtin/session_explore.rs index 69d4ced54..aacc16081 100644 --- a/crates/worker/src/feature/builtin/session_explore.rs +++ b/crates/worker/src/feature/builtin/session_explore.rs @@ -371,7 +371,7 @@ fn parse_entry_ref(value: &str) -> Result { fn parse_kind(value: &str) -> Result { ReferenceKind::parse(value).ok_or_else(|| { ToolError::InvalidArgument(format!( - "invalid kind {value:?}; expected user, assistant, or tool" + "invalid kind {value:?}; expected user, assistant, system, or tool" )) }) } diff --git a/crates/worker/src/feature/builtin/worker_observation.rs b/crates/worker/src/feature/builtin/worker_observation.rs index 325f89615..9b6d5488c 100644 --- a/crates/worker/src/feature/builtin/worker_observation.rs +++ b/crates/worker/src/feature/builtin/worker_observation.rs @@ -813,6 +813,7 @@ mod tests { _ => return None, }; Some(protocol::SessionSnapshotEntry { + use_policy: None, entry_id: format!("fake-{index:08}"), timestamp: index as u64, provenance: protocol::SessionEntryProvenance::LegacyUnknown, diff --git a/crates/worker/src/internal_worker.rs b/crates/worker/src/internal_worker.rs index 7ffc44b42..46400d52d 100644 --- a/crates/worker/src/internal_worker.rs +++ b/crates/worker/src/internal_worker.rs @@ -1499,6 +1499,7 @@ permission = "write" is_error: false, }, metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/worker/src/ipc/protocol_session.rs b/crates/worker/src/ipc/protocol_session.rs index 759c9c411..038770284 100644 --- a/crates/worker/src/ipc/protocol_session.rs +++ b/crates/worker/src/ipc/protocol_session.rs @@ -90,7 +90,7 @@ pub fn live_log_entry_event(entry: LogEntry) -> Option { } LogEntry::AnnotatedSystemItem { entry, .. } => { let entry_id = Some(entry.metadata.entry_id.0.clone()); - let value = serde_json::to_value(entry.item.public_item()) + let value = serde_json::to_value(entry.public_item()) .expect("public system input is serializable"); Some(Event::SystemItem { entry_id, @@ -348,6 +348,7 @@ mod tests { prompt_provenance: None, }, metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: entry_id.clone(), origin: session_store::LoggedSessionHistoryOrigin::BackendInstruction { operation_id: None, diff --git a/crates/worker/src/session_capture.rs b/crates/worker/src/session_capture.rs index c89570146..7e78a12fc 100644 --- a/crates/worker/src/session_capture.rs +++ b/crates/worker/src/session_capture.rs @@ -74,6 +74,7 @@ impl std::fmt::Display for SessionEntryRef { pub(crate) enum ReferenceKind { User, Assistant, + System, Tool, } @@ -82,6 +83,7 @@ impl ReferenceKind { match self { Self::User => "user", Self::Assistant => "assistant", + Self::System => "system", Self::Tool => "tool", } } @@ -90,6 +92,7 @@ impl ReferenceKind { match value { "user" => Some(Self::User), "assistant" | "agent" => Some(Self::Assistant), + "system" => Some(Self::System), "tool" => Some(Self::Tool), _ => None, } @@ -131,6 +134,7 @@ pub(crate) struct OverviewItem { #[derive(Debug, Clone)] pub(crate) struct ReferenceEntry { + use_policy: Option, pub id: SessionEntryRef, pub origin: SessionEntryProvenance, pub entry_range: [u64; 2], @@ -242,6 +246,7 @@ pub(crate) struct SessionEntryEvidence { #[derive(Debug, Clone)] struct CapturedHistoryEntry { + use_policy: Option, item: Item, entry_id: session_store::LoggedSessionHistoryEntryId, origin: SessionEntryProvenance, @@ -302,10 +307,12 @@ impl SessionCapture { is_error, attachments: _, } => Item::tool_result_item(call_id, summary, content, is_error), - // Observation deliberately excludes system items and - // controller errors from model-visible session evidence. - SessionSnapshotEntryData::SystemItem { .. } - | SessionSnapshotEntryData::Invoke { .. } + SessionSnapshotEntryData::SystemItem { content, .. } => { + entry.use_policy?; + Item::system_message(content) + } + // Controller errors are not model-visible evidence. + SessionSnapshotEntryData::Invoke { .. } | SessionSnapshotEntryData::Usage { .. } | SessionSnapshotEntryData::RunStats { .. } | SessionSnapshotEntryData::RunYielded { .. } @@ -314,6 +321,7 @@ impl SessionCapture { | SessionSnapshotEntryData::RunError { .. } => return None, }; Some(CapturedHistoryEntry { + use_policy: entry.use_policy, item, entry_id: session_store::LoggedSessionHistoryEntryId(entry.entry_id), origin: entry.provenance, @@ -328,6 +336,7 @@ impl SessionCapture { .into_iter() .enumerate() .map(|(index, item)| CapturedHistoryEntry { + use_policy: None, item, entry_id: session_store::LoggedSessionHistoryEntryId(format!("{index:08}")), origin: SessionEntryProvenance::LegacyUnknown, @@ -342,10 +351,21 @@ impl SessionCapture { ) -> Self { let entries = entries .into_iter() - .map(|entry| CapturedHistoryEntry { - item: entry.item, - entry_id: entry.annotation.entry_id, - origin: public_provenance(&entry.annotation.origin), + .map(|entry| { + let (item, use_policy) = if let Some(input) = &entry.annotation.system_input { + ( + Item::system_message(input.public_projection().body), + Some(input.public_use_policy()), + ) + } else { + (entry.item, None) + }; + CapturedHistoryEntry { + item, + use_policy, + entry_id: entry.annotation.entry_id, + origin: public_provenance(&entry.annotation.origin), + } }) .collect(); Self::from_captured_entries(segment_id, entries) @@ -364,7 +384,8 @@ impl SessionCapture { let entry_range = [idx as u64, idx as u64]; match item { Item::Message { role, content, .. } => { - let Some(kind) = message_reference_kind(&entry.origin, role) else { + let Some(kind) = message_reference_kind(&entry.origin, role, entry.use_policy) + else { continue; }; let text = content @@ -376,6 +397,7 @@ impl SessionCapture { let summary = truncate_chars(&text, 240); let id = SessionEntryRef::from_history_entry_id(&entry.entry_id); index.push(ReferenceEntry { + use_policy: entry.use_policy, id: id.clone(), origin: entry.origin.clone(), entry_range, @@ -386,7 +408,9 @@ impl SessionCapture { summary: summary.clone(), search_text: text.clone(), }); - if matches!(kind, ReferenceKind::User | ReferenceKind::Assistant) { + if entry.use_policy.is_none_or(|p| p.observation_readable) + && matches!(kind, ReferenceKind::User | ReferenceKind::Assistant) + { overview.push(OverviewItem { id: id.clone(), origin: entry.origin.clone(), @@ -403,6 +427,7 @@ impl SessionCapture { } => { let text = format!("{name}\n{arguments}"); index.push(ReferenceEntry { + use_policy: entry.use_policy, id: SessionEntryRef::from_history_entry_id(&entry.entry_id), origin: entry.origin.clone(), entry_range, @@ -430,6 +455,7 @@ impl SessionCapture { content.as_deref().unwrap_or_default(), ); index.push(ReferenceEntry { + use_policy: entry.use_policy, id: SessionEntryRef::from_history_entry_id(&entry.entry_id), origin: entry.origin.clone(), entry_range, @@ -510,6 +536,9 @@ impl SessionCapture { let mut hits = Vec::new(); for entry in &self.index { + if entry.use_policy.is_some_and(|p| !p.observation_readable) { + continue; + } if entry.entry_range[0] < min_entry_index || entry.entry_range[0] > max_entry_index { continue; } @@ -581,6 +610,9 @@ impl SessionCapture { }; for entry in selected { + if entry.use_policy.is_some_and(|p| !p.observation_readable) { + continue; + } if entries.len() >= max_items { truncated = true; break; @@ -695,21 +727,11 @@ impl SessionCapture { pub(crate) fn evidence_for(&self, id: &str) -> Option { let entry = self.index.iter().find(|entry| entry.id.as_str() == id)?; - let excerpt = self - .read( - ReadSelector::Id(id), - ReadOptions { - include_tools: true, - tool_part: ToolPart::Both, - detail: ReadDetail::Compact, - max_items: 1, - max_bytes: 2 * 1024, - }, - ) - .entries - .first() - .map(|entry| entry.text.clone()) - .unwrap_or_else(|| entry.summary.clone()); + if entry.use_policy.is_some_and(|p| !p.memory_evidence) { + return None; + } + let item = &self.entries.get(entry.entry_range[0] as usize)?.item; + let excerpt = render_item(item, entry, ReadDetail::Compact, 2 * 1024); Some(SessionEntryEvidence { segment_id: self.segment_id.clone(), entry_ref: entry.id.clone(), @@ -731,9 +753,8 @@ fn public_provenance(origin: &WorkerHistoryProvenance) -> SessionEntryProvenance } WorkerHistoryProvenance::WorkerInput { .. } => SessionEntryProvenance::WorkerInput, WorkerHistoryProvenance::FlowInstruction { .. } => SessionEntryProvenance::FlowInstruction, - WorkerHistoryProvenance::BackendInstruction { .. } => { - SessionEntryProvenance::BackendInstruction - } + WorkerHistoryProvenance::BackendInstruction { .. } + | WorkerHistoryProvenance::Provided { .. } => SessionEntryProvenance::BackendInstruction, WorkerHistoryProvenance::ModelOutput { .. } => SessionEntryProvenance::ModelOutput, WorkerHistoryProvenance::ToolOutput { .. } => SessionEntryProvenance::ToolOutput, WorkerHistoryProvenance::CompactionSummary @@ -746,7 +767,15 @@ fn public_provenance(origin: &WorkerHistoryProvenance) -> SessionEntryProvenance fn message_reference_kind( origin: &SessionEntryProvenance, provider_role: &Role, + use_policy: Option, ) -> Option { + if use_policy.is_some() { + return match provider_role { + Role::System => Some(ReferenceKind::System), + Role::User => Some(ReferenceKind::User), + Role::Assistant => Some(ReferenceKind::Assistant), + }; + } match origin { SessionEntryProvenance::HumanInput | SessionEntryProvenance::WorkerInput => { Some(ReferenceKind::User) @@ -950,6 +979,148 @@ mod tests { } } + #[test] + fn generic_public_input_observation_and_memory_policies_survive_store_restore_independently() { + use session_store::{ + FeatureInputPayload, FsStore, InputCompactionUse, InputEvidenceUse, + InputObservationUse, LogEntry, LoggedSystemHistoryEntry, PublicSystemItem, + PublicSystemPresentation, Store, SystemInputEnvelope, SystemInputMetadata, + SystemInputUsePolicy, + }; + let root = tempfile::tempdir().unwrap(); + let store = FsStore::new(root.path()).unwrap(); + let session = session_store::new_session_id(); + let segment = session_store::new_segment_id(); + store + .create_segment( + session, + segment, + &[LogEntry::AnnotatedSegmentStart { + ts: 1, + session_id: session, + system_prompt: None, + config: Default::default(), + history: vec![], + forked_from: None, + compacted_from: None, + }], + ) + .unwrap(); + let mut inputs = Vec::new(); + for (producer, observation, memory_evidence, public) in [ + ( + "same", + InputObservationUse::Readable, + InputEvidenceUse::Exclude, + true, + ), + ( + "same", + InputObservationUse::Hidden, + InputEvidenceUse::Include, + true, + ), + ( + "renamed", + InputObservationUse::Readable, + InputEvidenceUse::Include, + true, + ), + ( + "renamed", + InputObservationUse::Hidden, + InputEvidenceUse::Exclude, + true, + ), + ( + "private", + InputObservationUse::Readable, + InputEvidenceUse::Include, + false, + ), + ] { + let entry = LoggedSystemHistoryEntry::from_input( + WorkerHistoryProvenance::Provided { + producer: producer.into(), + }, + None, + SystemInputEnvelope::new( + "private model text", + SystemInputMetadata::new( + SystemInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence, + observation, + compaction: InputCompactionUse::ReferenceInput, + }, + Some( + FeatureInputPayload::encode( + "future:owner", + 99, + &serde_json::json!({ "secret": "private metadata" }), + ) + .unwrap(), + ), + public.then(|| PublicSystemItem { + kind: "supplied-context".into(), + body: "public context: do not extract this".into(), + presentation: PublicSystemPresentation::Context, + }), + ) + .unwrap(), + ) + .unwrap(), + ); + store + .append( + session, + segment, + &LogEntry::AnnotatedSystemItem { + ts: 2, + entry: entry.clone(), + extensions: vec![], + }, + ) + .unwrap(); + inputs.push(entry); + } + let saved = store.read_all(session, segment).unwrap(); + let restored = + crate::session_history::restore_history_entries(session, segment, &saved).unwrap(); + let snapshot = session_store::public_snapshot::project_session_snapshot(session, &saved); + let snapshot: SessionSnapshot = + serde_json::from_value(serde_json::to_value(snapshot).unwrap()).unwrap(); + for capture in [ + SessionCapture::from_history_entries(segment.to_string(), restored), + SessionCapture::from_session_snapshot(segment.to_string(), snapshot), + ] { + let hits = capture.search(&SearchOptions { + kind: Some(ReferenceKind::System), + ..Default::default() + }); + assert_eq!(hits.len(), 2); + for (index, input) in inputs.iter().enumerate() { + let reference = SessionEntryRef::from_history_entry_id(&input.metadata.entry_id); + let read = + capture.read(ReadSelector::Id(reference.as_str()), ReadOptions::default()); + assert_eq!(!read.entries.is_empty(), matches!(index, 0 | 2)); + let evidence = capture.evidence_for(reference.as_str()); + assert_eq!(evidence.is_some(), matches!(index, 1 | 2)); + for text in read + .entries + .iter() + .map(|entry| entry.text.as_str()) + .chain(evidence.iter().map(|entry| entry.excerpt.as_str())) + { + assert!(text.contains("public context: do not extract this")); + assert!(!text.contains("private model text")); + assert!(!text.contains("private metadata")); + assert!(!text.contains("future:owner")); + } + } + } + } + #[test] fn overview_contains_user_and_assistant_only() { let view = SessionCapture::new( diff --git a/crates/worker/src/session_history.rs b/crates/worker/src/session_history.rs index d416ccef1..232a8c92d 100644 --- a/crates/worker/src/session_history.rs +++ b/crates/worker/src/session_history.rs @@ -24,6 +24,7 @@ pub(crate) fn test_logged_history_entry(item: impl Into) -> LoggedHistoryE LoggedHistoryEntry { item: session_store::LoggedItem::from(item.into()), metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -38,6 +39,7 @@ pub(crate) fn test_logged_system_entry( session_store::LoggedSystemHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -58,6 +60,7 @@ pub(crate) fn metadata( derivation: Option, ) -> SessionHistoryMetadata { SessionHistoryMetadata { + system_input: None, entry_id: SessionHistoryEntryId::new(), origin, derivation, @@ -80,6 +83,7 @@ pub(crate) fn history_entry_with_id( HistoryEntry::new( item, SessionHistoryMetadata { + system_input: None, entry_id, origin, derivation: None, diff --git a/crates/worker/src/spawn/comm_tools.rs b/crates/worker/src/spawn/comm_tools.rs index 9fca770de..59ca3c167 100644 --- a/crates/worker/src/spawn/comm_tools.rs +++ b/crates/worker/src/spawn/comm_tools.rs @@ -77,6 +77,7 @@ mod tests { .into_iter() .enumerate() .map(|(index, value)| protocol::SessionSnapshotEntry { + use_policy: None, entry_id: format!("test-{index}"), timestamp: index as u64, provenance: protocol::SessionEntryProvenance::LegacyUnknown, diff --git a/crates/worker/src/spawn/registry.rs b/crates/worker/src/spawn/registry.rs index 05317f491..1d5b7dfb3 100644 --- a/crates/worker/src/spawn/registry.rs +++ b/crates/worker/src/spawn/registry.rs @@ -1338,6 +1338,7 @@ mod tests { is_error: false, }, metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/worker/src/wip.rs b/crates/worker/src/wip.rs index 8c4a0849e..1f12478d3 100644 --- a/crates/worker/src/wip.rs +++ b/crates/worker/src/wip.rs @@ -5502,6 +5502,7 @@ mod tests { entry: session_store::LoggedHistoryEntry { item: session_store::LoggedItem::from(saved), metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId::new(), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/worker/tests/compact_events_test.rs b/crates/worker/tests/compact_events_test.rs index 2aeb8f521..d72150e71 100644 --- a/crates/worker/tests/compact_events_test.rs +++ b/crates/worker/tests/compact_events_test.rs @@ -119,6 +119,7 @@ fn annotated(item: Item) -> session_store::LoggedHistoryEntry { session_store::LoggedHistoryEntry { item: session_store::LoggedItem::from(item), metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId::new(), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index dd256fea9..4bdf8a7f6 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -44108,6 +44108,7 @@ mod tests { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId(id.to_string()), origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".to_string(), @@ -44297,6 +44298,7 @@ mod tests { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { + system_input: None, entry_id: LoggedSessionHistoryEntryId(id.to_string()), origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".to_string(), diff --git a/crates/workspace-server/src/server/tests/subjektiv_journal_tests.rs b/crates/workspace-server/src/server/tests/subjektiv_journal_tests.rs index 8f83a29e0..fd0c89fea 100644 --- a/crates/workspace-server/src/server/tests/subjektiv_journal_tests.rs +++ b/crates/workspace-server/src/server/tests/subjektiv_journal_tests.rs @@ -479,6 +479,7 @@ fn seed_session( vec![session_store::LoggedHistoryEntry { item, metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId("read-source-original".into()), origin, derivation: None, @@ -1706,6 +1707,7 @@ impl SubjectRemovalFixture { .map(|text| session_store::LoggedHistoryEntry { item: message(text), metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId(text.into()), origin: session_store::LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, diff --git a/crates/workspace-server/src/server/tests/subjektiv_memory_journal_tests.rs b/crates/workspace-server/src/server/tests/subjektiv_memory_journal_tests.rs index abcc72632..efe44156f 100644 --- a/crates/workspace-server/src/server/tests/subjektiv_memory_journal_tests.rs +++ b/crates/workspace-server/src/server/tests/subjektiv_memory_journal_tests.rs @@ -67,6 +67,7 @@ impl Fixture { content: vec![session_store::LoggedContentPart::Text { text: text.into() }], }, metadata: session_store::LoggedSessionHistoryMetadata { + system_input: None, entry_id: session_store::LoggedSessionHistoryEntryId(format!("original-{index}")), origin: session_store::LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, diff --git a/web/workspace/src/lib/generated/protocol-validator.ts b/web/workspace/src/lib/generated/protocol-validator.ts index 2a2da1ea5..7b58a3b4b 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":{"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 allowlisted JSON form of `session_store::PublicSystemItem`,\nnot the storage record. Its descriptive `kind` is independent from the\ntyped `presentation` used for notification, reminder, or context rendering.\nPrivate Feature payloads and prompt provenance are never event data.\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"}; +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 allowlisted JSON form of `session_store::PublicSystemItem`,\nnot the storage record. Its descriptive `kind` is independent from the\ntyped `presentation` used for notification, reminder, or context rendering.\nPrivate Feature payloads and prompt provenance are never event data.\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"},"SessionEntryUsePolicy":{"additionalProperties":false,"description":"Public use decisions contain no Feature identity or private payload. Absence\ndenotes the older projection contract, not permission to expose System input.","properties":{"memory_evidence":{"type":"boolean"},"observation_readable":{"type":"boolean"}},"required":["observation_readable","memory_evidence"],"type":"object"},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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"},"use_policy":{"anyOf":[{"$ref":"#/$defs/SessionEntryUsePolicy"},{"type":"null"}]}},"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 8b6870f39..8223baf07 100644 --- a/web/workspace/src/lib/generated/protocol.ts +++ b/web/workspace/src/lib/generated/protocol.ts @@ -206,7 +206,7 @@ entry_id: string, /** * Timestamp copied from the durable log record that commits this entry. */ -timestamp: number, provenance: SessionEntryProvenance, derived_from?: Array, } & ({ "kind": "user_input", segments: Array, } | { "kind": "message", role: SessionMessageRole, content: Array, } | { "kind": "tool_call", call_id: string, name: string, arguments: string, } | { "kind": "tool_result", call_id: string, summary: string, content?: string | null, is_error: boolean, attachments?: Array, } | { "kind": "system_item", item_kind: string, content: string, data?: unknown, } | { "kind": "run_yielded", reason: RunYieldReason, active_run_turn_count: number, } | { "kind": "run_resumed", source: RunResumeSource, active_run_turn_count: number, } | { "kind": "invoke", trigger: InvokeKind, } | { "kind": "usage", input_tokens: number, cache_read_input_tokens: number, output_tokens: number, } | { "kind": "run_stats", elapsed_ms: number, requests: number, upload_tokens: number, output_tokens: number, } | { "kind": "run_cancelled" } | { "kind": "run_error", message: string, failure?: RunFailureKind | null, }); +timestamp: number, provenance: SessionEntryProvenance, derived_from?: Array, use_policy?: SessionEntryUsePolicy | null, } & ({ "kind": "user_input", segments: Array, } | { "kind": "message", role: SessionMessageRole, content: Array, } | { "kind": "tool_call", call_id: string, name: string, arguments: string, } | { "kind": "tool_result", call_id: string, summary: string, content?: string | null, is_error: boolean, attachments?: Array, } | { "kind": "system_item", item_kind: string, content: string, data?: unknown, } | { "kind": "run_yielded", reason: RunYieldReason, active_run_turn_count: number, } | { "kind": "run_resumed", source: RunResumeSource, active_run_turn_count: number, } | { "kind": "invoke", trigger: InvokeKind, } | { "kind": "usage", input_tokens: number, cache_read_input_tokens: number, output_tokens: number, } | { "kind": "run_stats", elapsed_ms: number, requests: number, upload_tokens: number, output_tokens: number, } | { "kind": "run_cancelled" } | { "kind": "run_error", message: string, failure?: RunFailureKind | null, }); export type SessionConversationTurn = { turn_id: string, entries: Array, }; From 6e729ed06c01595ba8e3736e5741d850162257f0 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 03:21:22 +0900 Subject: [PATCH 06/11] refactor: isolate Feature System input migration and compaction policy --- crates/session-store/src/history.rs | 2 +- .../session-store/src/legacy_session_log.rs | 316 +++++++++- .../src/legacy_session_log/tests.rs | 2 + .../tests/fixtures/v1-system-inputs.jsonl | 3 + .../tests/fixtures/v4-system-inputs.jsonl | 8 + .../src/legacy_session_log/tests/v5.rs | 596 ++++++++++++++++++ crates/session-store/src/public_material.rs | 23 +- .../src/public_material/tests.rs | 170 +++-- crates/session-store/src/system_item.rs | 134 +--- .../session-store/src/worker_session_store.rs | 206 +++++- .../src/subjektiv/journal/tests/handoff.rs | 2 +- crates/worker/src/controller.rs | 6 +- crates/worker/src/feature/builtin/journal.rs | 169 ++++- crates/worker/src/feature/builtin/memory.rs | 192 ++++-- crates/worker/src/feature/builtin/task/mod.rs | 10 +- crates/worker/src/feature/invocation.rs | 39 ++ crates/worker/src/hook.rs | 80 ++- crates/worker/src/ipc/interceptor.rs | 90 ++- crates/worker/src/ipc/protocol_session.rs | 48 +- crates/worker/src/session_history.rs | 30 +- crates/worker/src/skill.rs | 44 ++ crates/worker/src/subjektiv.rs | 66 +- crates/worker/src/worker.rs | 394 +++++++----- crates/worker/tests/compact_events_test.rs | 180 ++++-- .../worker/tests/feature_invocation_test.rs | 20 +- .../subjektiv_journal_connected_tests.rs | 36 +- tools/state-contract-name-exceptions.json | 6 +- 27 files changed, 2226 insertions(+), 646 deletions(-) create mode 100644 crates/session-store/src/legacy_session_log/tests/fixtures/v1-system-inputs.jsonl create mode 100644 crates/session-store/src/legacy_session_log/tests/fixtures/v4-system-inputs.jsonl create mode 100644 crates/session-store/src/legacy_session_log/tests/v5.rs diff --git a/crates/session-store/src/history.rs b/crates/session-store/src/history.rs index f25617b1d..67d753c13 100644 --- a/crates/session-store/src/history.rs +++ b/crates/session-store/src/history.rs @@ -95,7 +95,7 @@ pub struct LoggedSessionHistoryMetadata { } impl LoggedSessionHistoryMetadata { - fn validate_system_input(&self) -> Result<(), crate::SystemInputError> { + pub(crate) fn validate_system_input(&self) -> Result<(), crate::SystemInputError> { if matches!(self.origin, LoggedSessionHistoryOrigin::Provided { .. }) && self.system_input.is_none() { diff --git a/crates/session-store/src/legacy_session_log.rs b/crates/session-store/src/legacy_session_log.rs index 987a53984..10e247539 100644 --- a/crates/session-store/src/legacy_session_log.rs +++ b/crates/session-store/src/legacy_session_log.rs @@ -1,12 +1,12 @@ -//! Versioned decoder for Session schemas that predate canonical annotated history. +//! Versioned decoder for Session schemas v1-4, including frozen Feature wire. //! //! These types are intentionally private to `session-store`. Current writers, //! replay, and public projections use [`crate::LogEntry`] exclusively; only the -//! Worker Session schema migration is allowed to deserialize these shapes. +//! explicit Session schema migration is allowed to deserialize these shapes. use agen::llm_client::types::RequestConfig; use protocol::Segment; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; #[cfg(test)] mod tests; @@ -17,6 +17,212 @@ use crate::{ SegmentOrigin, SessionExtension, SessionId, SystemItem, }; +// Frozen v1-4 Feature wire. These types are deliberately not exported and are +// never used by LogEntry/SystemItem's current deserializers. Payload structs +// retain the owner's v1 shape rather than erasing typed fields into arbitrary JSON. +#[derive(Debug, Serialize, Deserialize)] +struct JournalStartupPayload { + snapshot: serde_json::Value, + snapshot_sha256: String, + prompt_provenance: crate::PromptRenderProvenance, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ResidentSummaryPayload { + #[serde(default)] + prompt_provenance: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct SubjectBehaviorPayload { + #[serde(default)] + behavior_md: String, + #[serde(default)] + prompt_provenance: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct SkillActivationPayload { + name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct InvocationResultPayload { + result: protocol::FeatureInvocationResult, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +enum LegacyFeatureSystemItem { + JournalStartup { + body: String, + #[serde(flatten)] + payload: JournalStartupPayload, + }, + ResidentSummaryRefresh { + body: String, + #[serde(flatten)] + payload: ResidentSummaryPayload, + }, + SubjectBehaviorRefresh { + body: String, + #[serde(flatten)] + payload: SubjectBehaviorPayload, + }, + SkillActivation { + body: String, + #[serde(flatten)] + payload: SkillActivationPayload, + }, + FeatureInvocationResult { + body: String, + #[serde(flatten)] + payload: InvocationResultPayload, + }, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum LegacySystemItem { + Feature(LegacyFeatureSystemItem), + // Includes already-migrated Input records for interrupted migration retry. + Core(SystemItem), +} + +impl LegacySystemItem { + fn into_entry( + self, + mut metadata: LoggedSessionHistoryMetadata, + ) -> Result { + use crate::InputCompactionUse::{ReferenceInput, Retain}; + let item = match self { + Self::Core(item) => item, + Self::Feature(feature) => { + metadata.validate_system_input()?; + if metadata.system_input.is_some() { + return Err(crate::SystemInputError::InconsistentRecord); + } + let (body, input_metadata) = match feature { + LegacyFeatureSystemItem::JournalStartup { body, payload } => { + migrated_feature_input( + body, + "journal_startup", + "journal:startup", + payload, + ReferenceInput, + )? + } + LegacyFeatureSystemItem::ResidentSummaryRefresh { body, payload } => { + migrated_feature_input( + body, + "resident_summary_refresh", + "memory:resident-summary", + payload, + Retain, + )? + } + LegacyFeatureSystemItem::SubjectBehaviorRefresh { body, payload } => { + migrated_feature_input( + body, + "subject_behavior_refresh", + "subject:behavior", + payload, + Retain, + )? + } + LegacyFeatureSystemItem::SkillActivation { body, payload } => { + migrated_feature_input( + body, + "skill_activation", + "skill:activation", + payload, + Retain, + )? + } + LegacyFeatureSystemItem::FeatureInvocationResult { body, payload } => { + migrated_feature_input( + body, + "feature_invocation_result", + "feature:invocation-result", + payload, + Retain, + )? + } + }; + // Identity/origin/derivation are retained, never re-inferred. + metadata.system_input = Some(input_metadata); + SystemItem::Input { body } + } + }; + let entry = LoggedSystemHistoryEntry { item, metadata }; + entry.validate_system_input()?; + Ok(entry) + } +} + +fn migrated_feature_input( + body: String, + label: &str, + schema: &str, + payload: T, + compaction: crate::InputCompactionUse, +) -> Result<(String, crate::SystemInputMetadata), crate::SystemInputError> { + let metadata = crate::SystemInputMetadata::new( + migration_policy(compaction), + Some(crate::FeatureInputPayload::encode(schema, 1, &payload)?), + Some(crate::PublicSystemItem { + kind: label.into(), + body: body.clone(), + presentation: crate::PublicSystemPresentation::Context, + }), + )?; + Ok((body, metadata)) +} + +/// Annotated old wire used by v3/v4, and by already-converted files when an +/// older marker survived an interrupted migration. No ordinary decoder uses it. +#[derive(Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +enum LegacyAnnotatedLogEntry { + AnnotatedSystemItem { + ts: u64, + entry: LegacySystemHistoryEntry, + #[serde(default)] + extensions: Vec, + }, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct LegacySystemHistoryEntry { + item: LegacySystemItem, + metadata: LoggedSessionHistoryMetadata, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum LegacyAnnotatedOrCurrent { + Annotated(LegacyAnnotatedLogEntry), + Current(LogEntry), +} + +impl LegacyAnnotatedOrCurrent { + fn into_current(self) -> Result { + match self { + Self::Annotated(LegacyAnnotatedLogEntry::AnnotatedSystemItem { + ts, + entry, + extensions, + }) => Ok(LogEntry::AnnotatedSystemItem { + ts, + entry: entry.item.into_entry(entry.metadata)?, + extensions, + }), + Self::Current(entry) => Ok(entry), + } + } +} + #[derive(Debug, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] enum LegacyHistoryLogEntry { @@ -47,7 +253,9 @@ enum LegacyHistoryLogEntry { }, SystemItem { ts: u64, - item: SystemItem, + item: LegacySystemItem, + #[serde(default)] + extensions: Vec, }, } @@ -57,7 +265,7 @@ enum LegacyHistoryLogEntry { #[serde(untagged)] enum LegacySessionLogEntryV1 { History(LegacyHistoryLogEntry), - Current(LogEntry), + Current(LegacyAnnotatedOrCurrent), } /// Schema v2 retained the v1 history shapes while adding non-history records. @@ -67,7 +275,7 @@ enum LegacySessionLogEntryV1 { #[serde(untagged)] enum LegacySessionLogEntryV2 { History(LegacyHistoryLogEntry), - Current(LogEntry), + Current(LegacyAnnotatedOrCurrent), } pub(crate) fn decode_entry( @@ -86,15 +294,16 @@ pub(crate) fn decode_entry( LegacySessionLogEntryV2::History(entry) => Entry::History(entry), LegacySessionLogEntryV2::Current(entry) => Entry::Current(entry), }, - 3 => Entry::Current(serde_json::from_str(line)?), + 3 | 4 => Entry::Current(serde_json::from_str(line)?), _ => unreachable!("legacy decoder called for unsupported schema {schema_version}"), }; - Ok(match entry { + match entry { Entry::History(entry) => { canonicalize_history_entry(session_id, segment_id, line_index, entry) } - Entry::Current(entry) => entry, - }) + Entry::Current(entry) => entry.into_current(), + } + .map_err(serde::de::Error::custom) } /// Schema 3 used DerivedSummary for Host compaction seeds. Only the trusted @@ -152,9 +361,75 @@ pub(crate) fn migrate_compaction_origins( } } +/// Explicit v1-4 -> v5 boundary. No Feature inference belongs in ordinary +/// readers/restoration. Invalid or oversized payloads fail migration; the generic +/// payload/body/public limits are deliberately not relaxed for legacy data. +pub(crate) fn migrate_system_inputs(entry: &mut LogEntry) -> Result<(), crate::SystemInputError> { + entry.validate_system_inputs()?; + match entry { + // Feature wire has already been converted by the explicit old-schema + // decoder. Ordinary LogEntry can contain only generic Input/core events. + LogEntry::AnnotatedSystemItem { entry, .. } => entry.validate_system_input()?, + LogEntry::AnnotatedSegmentStart { history, .. } + | LogEntry::AnnotatedUserInput { history, .. } => { + for entry in history { + migrate_flattened_system_input(entry)?; + } + } + LogEntry::AnnotatedAssistantItem { entry, .. } + | LogEntry::AnnotatedToolResult { entry, .. } + | LogEntry::ToolResultCorrected { entry, .. } => { + migrate_flattened_system_input(entry)?; + } + _ => {} + } + entry.validate_system_inputs() +} + +fn migration_policy(compaction: crate::InputCompactionUse) -> crate::SystemInputUsePolicy { + crate::SystemInputUsePolicy { + recording_material: crate::InputEvidenceUse::Exclude, + memory_evidence: crate::InputEvidenceUse::Exclude, + observation: crate::InputObservationUse::Hidden, + compaction, + } +} + +fn migrate_flattened_system_input( + entry: &mut LoggedHistoryEntry, +) -> Result<(), crate::SystemInputError> { + // Check before supplying legacy policy: Provided without metadata is a + // malformed NEW record, not permission to synthesize a producer policy. + entry.validate_system_input()?; + if entry.metadata.system_input.is_none() + && matches!( + entry.item, + LoggedItem::Message { + role: crate::LoggedRole::System, + .. + } + ) + { + let compaction = match entry.metadata.origin { + LoggedSessionHistoryOrigin::JournalContext => crate::InputCompactionUse::ReferenceInput, + LoggedSessionHistoryOrigin::CompactionSummary => { + crate::InputCompactionUse::OpeningSummary + } + _ => crate::InputCompactionUse::Retain, + }; + // No Feature payload or public projection can be recovered from text. + entry.metadata.system_input = Some(crate::SystemInputMetadata::new( + migration_policy(compaction), + None, + None, + )?); + } + entry.validate_system_input() +} + enum Entry { History(LegacyHistoryLogEntry), - Current(LogEntry), + Current(LegacyAnnotatedOrCurrent), } fn legacy_metadata( @@ -182,8 +457,8 @@ fn canonicalize_history_entry( segment_id: SegmentId, line_index: usize, entry: LegacyHistoryLogEntry, -) -> LogEntry { - match entry { +) -> Result { + Ok(match entry { LegacyHistoryLogEntry::SegmentStart { ts, session_id, @@ -237,13 +512,14 @@ fn canonicalize_history_entry( metadata: legacy_metadata(segment_id, line_index, 0), }, }, - LegacyHistoryLogEntry::SystemItem { ts, item } => LogEntry::AnnotatedSystemItem { + LegacyHistoryLogEntry::SystemItem { ts, - entry: LoggedSystemHistoryEntry { - item, - metadata: legacy_metadata(segment_id, line_index, 0), - }, - extensions: Vec::new(), + item, + extensions, + } => LogEntry::AnnotatedSystemItem { + ts, + entry: item.into_entry(legacy_metadata(segment_id, line_index, 0))?, + extensions, }, - } + }) } diff --git a/crates/session-store/src/legacy_session_log/tests.rs b/crates/session-store/src/legacy_session_log/tests.rs index 314ee6d5f..d7914c194 100644 --- a/crates/session-store/src/legacy_session_log/tests.rs +++ b/crates/session-store/src/legacy_session_log/tests.rs @@ -1,5 +1,7 @@ use super::*; +mod v5; + fn seed( origin: LoggedSessionHistoryOrigin, compacted: bool, diff --git a/crates/session-store/src/legacy_session_log/tests/fixtures/v1-system-inputs.jsonl b/crates/session-store/src/legacy_session_log/tests/fixtures/v1-system-inputs.jsonl new file mode 100644 index 000000000..742ab5557 --- /dev/null +++ b/crates/session-store/src/legacy_session_log/tests/fixtures/v1-system-inputs.jsonl @@ -0,0 +1,3 @@ +{"kind":"system_item","ts":1,"item":{"kind":"skill_activation","name":"legacy-skill","body":"legacy skill body"}} +{"kind":"user_input","ts":2,"segments":[{"kind":"text","content":"legacy user body"}],"extensions":[{"domain":"old","payload":{"kept":true}}]} +{"kind":"assistant_item","ts":3,"item":{"kind":"message","role":"assistant","content":[{"kind":"text","text":"legacy reply"}]}} diff --git a/crates/session-store/src/legacy_session_log/tests/fixtures/v4-system-inputs.jsonl b/crates/session-store/src/legacy_session_log/tests/fixtures/v4-system-inputs.jsonl new file mode 100644 index 000000000..59a391d01 --- /dev/null +++ b/crates/session-store/src/legacy_session_log/tests/fixtures/v4-system-inputs.jsonl @@ -0,0 +1,8 @@ +{"kind":"annotated_system_item","ts":101,"entry":{"item":{"kind":"journal_startup","body":"journal body\nunchanged","snapshot":{"entries":[{"text":"private snapshot"}]},"snapshot_sha256":"snapshot-digest","prompt_provenance":{"workspace_id":"workspace","source_digest":"source","projection_digest":"projection","logical_name":"journal_startup"}},"metadata":{"entry_id":"journal-id","origin":{"kind":"legacy_unknown"},"derivation":{"sources":["source-id"]}}},"extensions":[{"domain":"migration-fixture","payload":{"kept":true}}]} +{"kind":"annotated_system_item","ts":102,"entry":{"item":{"kind":"resident_summary_refresh","body":"resident body"},"metadata":{"entry_id":"resident-id","origin":{"kind":"derived_summary"}}}} +{"kind":"annotated_system_item","ts":103,"entry":{"item":{"kind":"subject_behavior_refresh","body":"rendered subject body","behavior_md":"exact behavior","prompt_provenance":{"source_digest":"subject-source","projection_digest":"subject-projection","logical_name":"subject_behavior"}},"metadata":{"entry_id":"subject-id","origin":{"kind":"backend_instruction","operation_id":"op"}}}} +{"kind":"annotated_system_item","ts":104,"entry":{"item":{"kind":"skill_activation","body":"skill body","name":"example-skill"},"metadata":{"entry_id":"skill-id","origin":{"kind":"legacy_unknown"}}}} +{"kind":"annotated_system_item","ts":105,"entry":{"item":{"kind":"feature_invocation_result","body":"result body","result":{"invocation_id":"invoke-id","identity":"builtin:test/prepare","status":"outcome_unknown","message":"may have committed","context":null}},"metadata":{"entry_id":"result-id","origin":{"kind":"legacy_unknown"}}}} +{"kind":"annotated_user_input","ts":106,"segments":[],"history":[{"item":{"kind":"message","role":"system","content":[{"kind":"text","text":"[Journal] prefix is not Feature evidence"}]},"metadata":{"entry_id":"flat-journal","origin":{"kind":"journal_context"},"derivation":{"sources":["journal-id"]}}},{"item":{"kind":"message","role":"system","content":[{"kind":"text","text":"summary body"}]},"metadata":{"entry_id":"flat-summary","origin":{"kind":"compaction_summary"}}},{"item":{"kind":"message","role":"system","content":[{"kind":"text","text":"[Skill: example] never infer Feature"}]},"metadata":{"entry_id":"flat-other","origin":{"kind":"legacy_unknown"}}}]} +{"kind":"annotated_system_item","ts":107,"entry":{"item":{"kind":"notification","message":"raw message","body":"wrapped message"},"metadata":{"entry_id":"core-id","origin":{"kind":"legacy_unknown"}}}} +{"kind":"annotated_system_item","ts":108,"entry":{"item":{"kind":"input","body":"already generic body"},"metadata":{"entry_id":"generic-id","origin":{"kind":"provided","producer":"future-owner"},"derivation":{"sources":["source-id"]},"system_input":{"usage":{"recording_material":"include","memory_evidence":"exclude","observation":"readable","compaction":"retain"},"feature_payload":{"schema":"future:unknown","version":99,"value":{"opaque":"retained"}},"public":null}}}} diff --git a/crates/session-store/src/legacy_session_log/tests/v5.rs b/crates/session-store/src/legacy_session_log/tests/v5.rs new file mode 100644 index 000000000..f197eeec9 --- /dev/null +++ b/crates/session-store/src/legacy_session_log/tests/v5.rs @@ -0,0 +1,596 @@ +//! Fixed old wire bytes, not fixtures generated by the current serializer. +use super::*; +use crate::{FsStore, InputCompactionUse, Store, WorkerSessionStore}; +use serde_json::{Value, json}; +use std::{fs, path::Path}; + +const V4: &str = include_str!("fixtures/v4-system-inputs.jsonl"); +const V1: &str = include_str!("fixtures/v1-system-inputs.jsonl"); + +fn decode_fixture(version: u32, source: &str, segment: SegmentId) -> Vec { + source + .lines() + .enumerate() + .map(|(index, line)| decode_entry(version, line, SessionId::nil(), segment, index).unwrap()) + .collect() +} + +fn encode(entries: &[LogEntry]) -> Vec { + let mut bytes = Vec::new(); + for entry in entries { + serde_json::to_writer(&mut bytes, entry).unwrap(); + bytes.push(b'\n'); + } + bytes +} + +fn migrate(entries: &mut [LogEntry]) { + for entry in entries { + migrate_system_inputs(entry).unwrap(); + entry.validate_system_inputs().unwrap(); + } +} + +fn write_worker(root: &Path, version: u32, source: &[u8]) -> (SessionId, SegmentId) { + let session = SessionId::now_v7(); + let segment = SegmentId::from_u128(1); + fs::create_dir_all(root.join("segments")).unwrap(); + fs::write( + root.join("session.json"), + serde_json::to_vec(&json!({ + "schema_version": version, "session_id": session, + })) + .unwrap(), + ) + .unwrap(); + fs::write( + root.join("segments").join(format!("{segment}.jsonl")), + source, + ) + .unwrap(); + (session, segment) +} + +fn assert_marker(root: &Path, version: u32) { + let manifest: Value = + serde_json::from_slice(&fs::read(root.join("session.json")).unwrap()).unwrap(); + assert_eq!(manifest["schema_version"], version); +} + +#[test] +fn v4_fixed_wire_preserves_identity_body_payload_public_and_independent_policy() { + let mut entries = decode_fixture(4, V4, SegmentId::nil()); + let before = entries.clone(); + migrate(&mut entries); + let schemas = [ + "journal:startup", + "memory:resident-summary", + "subject:behavior", + "skill:activation", + "feature:invocation-result", + ]; + let old_wire: Vec = V4 + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect(); + for index in 0..5 { + let LogEntry::AnnotatedSystemItem { + ts, + entry, + extensions, + } = &entries[index] + else { + panic!() + }; + let old = &old_wire[index]; + let old_item = &old["entry"]["item"]; + let old_metadata: LoggedSessionHistoryMetadata = + serde_json::from_value(old["entry"]["metadata"].clone()).unwrap(); + let old_ext: Vec = old + .get("extensions") + .map(|value| serde_json::from_value(value.clone()).unwrap()) + .unwrap_or_default(); + assert_eq!(*ts, old["ts"].as_u64().unwrap()); + assert_eq!(extensions, &old_ext); + assert!(matches!(entry.item, SystemItem::Input { .. })); + assert_eq!( + entry.item.history_text(), + old_item["body"].as_str().unwrap() + ); + assert_eq!(entry.metadata.entry_id, old_metadata.entry_id); + assert_eq!(entry.metadata.origin, old_metadata.origin); + assert_eq!(entry.metadata.derivation, old_metadata.derivation); + let metadata = entry.metadata.system_input.as_ref().unwrap(); + let public = metadata.public_item().unwrap(); + assert_eq!(public.kind, old_item["kind"].as_str().unwrap()); + assert_eq!(public.body, old_item["body"].as_str().unwrap()); + assert_eq!( + public.presentation, + crate::PublicSystemPresentation::Context + ); + assert_eq!( + metadata.usage(), + migration_policy(if index == 0 { + InputCompactionUse::ReferenceInput + } else { + InputCompactionUse::Retain + }) + ); + let payload = metadata.feature_payload().unwrap(); + assert_eq!(payload.schema(), schemas[index]); + assert_eq!(payload.version(), 1); + let value: Value = payload.decode(schemas[index], 1).unwrap(); + // Independently require each frozen owner's typed v1 payload contract. + match index { + 0 => { + let _: JournalStartupPayload = payload.decode(schemas[index], 1).unwrap(); + } + 1 => { + let _: ResidentSummaryPayload = payload.decode(schemas[index], 1).unwrap(); + } + 2 => { + let _: SubjectBehaviorPayload = payload.decode(schemas[index], 1).unwrap(); + } + 3 => { + let _: SkillActivationPayload = payload.decode(schemas[index], 1).unwrap(); + } + 4 => { + let _: InvocationResultPayload = payload.decode(schemas[index], 1).unwrap(); + } + _ => unreachable!(), + } + let mut expected = old_item.clone(); + expected.as_object_mut().unwrap().remove("kind"); + expected.as_object_mut().unwrap().remove("body"); + // Optional provenance is an explicit Option in the v1 payload contract. + if index == 1 { + expected["prompt_provenance"] = Value::Null; + } + if index == 4 { + let result: protocol::FeatureInvocationResult = + serde_json::from_value(old_item["result"].clone()).unwrap(); + let typed: InvocationResultPayload = payload.decode(schemas[index], 1).unwrap(); + assert_eq!(typed.result, result); + // Preserve the protocol's typed serialization contract, including + // its canonical representation of optional result context. + expected["result"] = serde_json::to_value(result).unwrap(); + } + assert_eq!(value, expected); + assert!(value.get("body").is_none()); + } + let LogEntry::AnnotatedUserInput { history, .. } = &entries[5] else { + panic!() + }; + let LogEntry::AnnotatedUserInput { history: old, .. } = &before[5] else { + panic!() + }; + for (index, compaction) in [ + InputCompactionUse::ReferenceInput, + InputCompactionUse::OpeningSummary, + InputCompactionUse::Retain, + ] + .into_iter() + .enumerate() + { + assert_eq!(history[index].item, old[index].item); + assert_eq!( + history[index].metadata.entry_id, + old[index].metadata.entry_id + ); + assert_eq!(history[index].metadata.origin, old[index].metadata.origin); + assert_eq!( + history[index].metadata.derivation, + old[index].metadata.derivation + ); + let metadata = history[index].metadata.system_input.as_ref().unwrap(); + assert_eq!(metadata.usage(), migration_policy(compaction)); + assert!(metadata.public_item().is_none()); + assert!(metadata.feature_payload().is_none()); + } + // Core notification is unchanged; existing generic Input and unknown valid + // owner payload are retained without decoding/reclassifying a Feature. + assert_eq!(encode(&entries[6..]), encode(&before[6..])); + let once = encode(&entries); + migrate(&mut entries); + assert_eq!(encode(&entries), once); +} + +#[test] +fn v1_through_v4_worker_migration_reopen_and_mixed_file_retry() { + for version in 1..=4 { + let source = if version <= 2 { V1 } else { V4 }; + let root = tempfile::tempdir().unwrap(); + let (session, segment) = write_worker(root.path(), version, source.as_bytes()); + let path = root + .path() + .join("segments") + .join(format!("{segment}.jsonl")); + // Simulate an interrupted publication: v5 records precede legacy records + // in one file, and a fully new file coexists with an untouched old file. + let mut converted = decode_fixture(version, source, segment); + migrate(&mut converted); + let mut mixed = encode(&converted[..1]); + mixed.extend_from_slice(source.split_once('\n').unwrap().1.as_bytes()); + fs::write(&path, mixed).unwrap(); + let new_path = root + .path() + .join("segments") + .join(format!("{}.jsonl", SegmentId::from_u128(2))); + let new_bytes = encode(&converted); + fs::write(&new_path, &new_bytes).unwrap(); + let old_path = root + .path() + .join("segments") + .join(format!("{}.jsonl", SegmentId::from_u128(3))); + fs::write(&old_path, source).unwrap(); + + let store = WorkerSessionStore::new(root.path()).unwrap(); + assert_marker(root.path(), 5); + let restored = store.read_all(session, segment).unwrap(); + assert_eq!(encode(&restored), new_bytes); + assert_eq!(fs::read(&new_path).unwrap(), new_bytes); + let once = fs::read(&path).unwrap(); + let old_once = fs::read(&old_path).unwrap(); + drop(store); + let reopened = WorkerSessionStore::new(root.path()).unwrap(); + assert_eq!(encode(&reopened.read_all(session, segment).unwrap()), once); + assert_eq!(fs::read(&path).unwrap(), once); + assert_eq!(fs::read(&old_path).unwrap(), old_once); + } +} + +#[test] +fn all_segments_preflight_keeps_old_bytes_on_decode_or_conversion_failure() { + let mut oversized: Value = serde_json::from_str(V4.lines().next().unwrap()).unwrap(); + oversized["entry"]["item"]["snapshot"] = + json!({"large": "x".repeat(crate::system_input::MAX_FEATURE_PAYLOAD_BYTES + 1)}); + let mut unknown: Value = serde_json::from_str(V4.lines().next().unwrap()).unwrap(); + unknown["entry"]["item"]["kind"] = json!("unsupported_future_feature"); + let mut malformed = unknown.clone(); + malformed["entry"]["item"] = json!({"kind":"journal_startup", "body":"missing snapshot"}); + let mut missing_metadata: Value = serde_json::from_str(V4.lines().last().unwrap()).unwrap(); + missing_metadata["entry"]["metadata"] + .as_object_mut() + .unwrap() + .remove("system_input"); + let mut missing_input = missing_metadata.clone(); + missing_input["entry"]["metadata"]["origin"] = json!({"kind":"legacy_unknown"}); + let mut invalid_policy: Value = serde_json::from_str(V4.lines().last().unwrap()).unwrap(); + invalid_policy["entry"]["metadata"]["system_input"]["usage"]["compaction"] = json!("unknown"); + let mut invalid_payload = invalid_policy.clone(); + invalid_payload["entry"]["metadata"]["system_input"]["usage"]["compaction"] = json!("retain"); + invalid_payload["entry"]["metadata"]["system_input"]["feature_payload"]["version"] = json!(0); + let mut flat_provided: Value = serde_json::from_str(V4.lines().nth(5).unwrap()).unwrap(); + flat_provided["history"][0]["metadata"]["origin"] = + json!({"kind":"provided", "producer":"owner"}); + for bad in [ + "{broken-json}\n".to_owned(), + format!("{}\n", oversized), + format!("{}\n", unknown), + format!("{}\n", malformed), + format!("{}\n", missing_metadata), + format!("{}\n", invalid_policy), + format!("{}\n", invalid_payload), + format!("{}\n", flat_provided), + format!("{}\n", missing_input), + ] { + let root = tempfile::tempdir().unwrap(); + let (_, segment) = write_worker(root.path(), 4, V4.as_bytes()); + let first = root + .path() + .join("segments") + .join(format!("{segment}.jsonl")); + let later = root + .path() + .join("segments") + .join(format!("{}.jsonl", SegmentId::from_u128(2))); + fs::write(&later, &bad).unwrap(); + let marker = fs::read(root.path().join("session.json")).unwrap(); + assert!(WorkerSessionStore::new(root.path()).is_err()); + assert_eq!(fs::read(&first).unwrap(), V4.as_bytes()); + assert_eq!(fs::read(&later).unwrap(), bad.as_bytes()); + assert_eq!(fs::read(root.path().join("session.json")).unwrap(), marker); + } +} + +#[test] +fn unsupported_schema_preserves_all_old_bytes() { + for version in [0, 6, 99] { + let root = tempfile::tempdir().unwrap(); + let (_, segment) = write_worker(root.path(), version, V4.as_bytes()); + let path = root + .path() + .join("segments") + .join(format!("{segment}.jsonl")); + let marker = fs::read(root.path().join("session.json")).unwrap(); + assert!(WorkerSessionStore::new(root.path()).is_err()); + assert_eq!(fs::read(&path).unwrap(), V4.as_bytes()); + assert_eq!(fs::read(root.path().join("session.json")).unwrap(), marker); + } +} + +#[test] +fn standalone_store_uses_same_v1_through_v4_migration_and_reopens() { + // None represents the existing unmarked annotated standalone layout (v3). + for version in [None, Some(1), Some(2), Some(3), Some(4)] { + let root = tempfile::tempdir().unwrap(); + let session = SessionId::now_v7(); + let segment = SegmentId::from_u128(1); + let dir = root.path().join(session.to_string()); + fs::create_dir_all(&dir).unwrap(); + let source = if version.is_some_and(|v| v <= 2) { + V1 + } else { + V4 + }; + let path = dir.join(format!("{segment}.jsonl")); + fs::write(&path, source).unwrap(); + if let Some(version) = version { + fs::write(dir.join("session-schema.json"), version.to_string()).unwrap(); + } + let store = FsStore::new(root.path()).unwrap(); + let entries = store.read_all(session, segment).unwrap(); + for entry in entries { + entry.validate_system_inputs().unwrap(); + } + assert_eq!( + fs::read_to_string(dir.join("session-schema.json")) + .unwrap() + .trim(), + "5" + ); + let once = fs::read(&path).unwrap(); + drop(store); + FsStore::new(root.path()).unwrap(); + assert_eq!(fs::read(&path).unwrap(), once); + } +} + +#[test] +fn compaction_origin_upgrade_is_retained_only_for_pre_v4_sources() { + for version in [3, 4] { + let root = tempfile::tempdir().unwrap(); + let creation = seed( + LoggedSessionHistoryOrigin::DerivedSummary, + true, + crate::LoggedRole::System, + "[Compacted context summary]\n\nold summary", + ); + let inherited = seed( + LoggedSessionHistoryOrigin::DerivedSummary, + false, + crate::LoggedRole::System, + "[Compacted context summary]\n\nold summary", + ); + let (session, segment) = + write_worker(root.path(), version, &encode(&[creation, inherited])); + let store = WorkerSessionStore::new(root.path()).unwrap(); + for record in store.read_all(session, segment).unwrap() { + let LogEntry::AnnotatedSegmentStart { history, .. } = record else { + panic!() + }; + let metadata = &history[0].metadata; + assert_eq!(metadata.entry_id.0, "summary"); + assert_eq!(metadata.derivation.as_ref().unwrap().sources[0].0, "source"); + assert_eq!( + metadata.origin, + if version == 3 { + LoggedSessionHistoryOrigin::CompactionSummary + } else { + LoggedSessionHistoryOrigin::DerivedSummary + } + ); + assert_eq!( + metadata.system_input.as_ref().unwrap().usage().compaction, + if version == 3 { + InputCompactionUse::OpeningSummary + } else { + InputCompactionUse::Retain + } + ); + } + } +} + +#[test] +fn standalone_preflight_and_unsupported_marker_leave_sources_unchanged() { + for version in [4, 99] { + let root = tempfile::tempdir().unwrap(); + let session = SessionId::now_v7(); + let dir = root.path().join(session.to_string()); + fs::create_dir_all(&dir).unwrap(); + let first = dir.join(format!("{}.jsonl", SegmentId::from_u128(1))); + let later = dir.join(format!("{}.jsonl", SegmentId::from_u128(2))); + fs::write(&first, V4).unwrap(); + fs::write(&later, "{broken-json}\n").unwrap(); + let marker = version.to_string(); + fs::write(dir.join("session-schema.json"), &marker).unwrap(); + assert!(FsStore::new(root.path()).is_err()); + assert_eq!(fs::read(&first).unwrap(), V4.as_bytes()); + assert_eq!(fs::read_to_string(&later).unwrap(), "{broken-json}\n"); + assert_eq!( + fs::read_to_string(dir.join("session-schema.json")).unwrap(), + marker + ); + } +} + +#[test] +fn current_v5_rejects_all_legacy_feature_kinds_without_rewriting_sources() { + for line in V4.lines().take(5) { + // Current serde, writable open and read-only restore share the same + // current wire contract. None may dispatch to the old Feature decoder. + assert!(serde_json::from_str::(line).is_err()); + let bytes = format!("{line}\n"); + let root = tempfile::tempdir().unwrap(); + let (session, segment) = write_worker(root.path(), 5, bytes.as_bytes()); + let path = root + .path() + .join("segments") + .join(format!("{segment}.jsonl")); + let marker = fs::read(root.path().join("session.json")).unwrap(); + let readonly = WorkerSessionStore::open_read_only(root.path()).unwrap(); + assert!(readonly.read_all_read_only(session, segment).is_err()); + assert!(WorkerSessionStore::new(root.path()).is_err()); + assert_eq!(fs::read(&path).unwrap(), bytes.as_bytes()); + assert_eq!(fs::read(root.path().join("session.json")).unwrap(), marker); + + let standalone = tempfile::tempdir().unwrap(); + let dir = standalone.path().join(session.to_string()); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join(format!("{segment}.jsonl")); + fs::write(&path, &bytes).unwrap(); + fs::write(dir.join("session-schema.json"), "5").unwrap(); + assert!(FsStore::new(standalone.path()).is_err()); + assert_eq!(fs::read(&path).unwrap(), bytes.as_bytes()); + assert_eq!( + fs::read_to_string(dir.join("session-schema.json")).unwrap(), + "5" + ); + } +} + +#[test] +fn v1_v2_feature_wire_uses_the_same_typed_payloads_and_stable_legacy_identity() { + let segment = SegmentId::from_u128(42); + for version in [1, 2] { + for (index, line) in V4.lines().take(5).enumerate() { + let annotated: Value = serde_json::from_str(line).unwrap(); + let wire = json!({ + "kind": "system_item", "ts": annotated["ts"], + "item": annotated["entry"]["item"], + "extensions": [{"domain":"old-owner", "payload":{"kept":true}}], + }) + .to_string(); + let entry = decode_entry(version, &wire, SessionId::nil(), segment, index).unwrap(); + let retry = decode_entry(version, &wire, SessionId::nil(), segment, index).unwrap(); + assert_eq!(encode(&[entry.clone()]), encode(&[retry])); + let LogEntry::AnnotatedSystemItem { + ts, + entry, + extensions, + } = entry + else { + panic!() + }; + assert_eq!(ts, annotated["ts"].as_u64().unwrap()); + assert!(matches!(entry.item, SystemItem::Input { .. })); + assert_eq!( + entry.item.history_text(), + annotated["entry"]["item"]["body"].as_str().unwrap() + ); + assert_eq!( + entry.metadata.entry_id, + legacy_metadata(segment, index, 0).entry_id + ); + assert_eq!( + entry.metadata.origin, + LoggedSessionHistoryOrigin::LegacyUnknown + ); + assert!(entry.metadata.derivation.is_none()); + assert_eq!(extensions[0].domain, "old-owner"); + assert_eq!(extensions[0].payload, json!({"kept":true})); + let LogEntry::AnnotatedSystemItem { entry: v4, .. } = + decode_entry(4, line, SessionId::nil(), segment, index).unwrap() + else { + panic!() + }; + assert_eq!(entry.metadata.system_input, v4.metadata.system_input); + } + } +} + +#[test] +fn migration_rejects_malformed_typed_feature_fields_instead_of_retaining_unchecked_json() { + let mut items: Vec = V4 + .lines() + .take(5) + .map(|line| serde_json::from_str::(line).unwrap()["entry"]["item"].clone()) + .collect(); + items[0]["snapshot_sha256"] = json!(17); + items[1]["prompt_provenance"] = json!({"logical_name":"missing digests"}); + items[2]["behavior_md"] = json!(["not a string"]); + items[3]["name"] = json!({"not":"a name"}); + items[4]["result"]["status"] = json!("unsupported_status"); + for version in 1..=4 { + for item in &items { + let wire = if version <= 2 { + json!({"kind":"system_item", "ts":1, "item":item}) + } else { + json!({"kind":"annotated_system_item", "ts":1, "entry":{ + "item":item, "metadata":{ + "entry_id":"malformed-id", "origin":{"kind":"legacy_unknown"}, + }, + }}) + } + .to_string(); + assert!(decode_entry(version, &wire, SessionId::nil(), SegmentId::nil(), 0).is_err()); + } + } +} + +#[test] +fn frozen_behavior_counter_is_accepted_only_at_the_old_schema_boundary() { + for version in 1..=4 { + let item = json!({ + "kind":"subject_behavior_refresh", "body":"exact archived body", + "behavior_revision":7, + }); + let wire = if version <= 2 { + json!({"kind":"system_item", "ts":1, "item":item}) + } else { + json!({"kind":"annotated_system_item", "ts":1, "entry":{ + "item":item, "metadata":{ + "entry_id":"behavior-id", "origin":{"kind":"legacy_unknown"}, + }, + }}) + } + .to_string(); + assert!(serde_json::from_str::(&wire).is_err()); + let LogEntry::AnnotatedSystemItem { entry, .. } = + decode_entry(version, &wire, SessionId::nil(), SegmentId::nil(), 0).unwrap() + else { + panic!() + }; + assert_eq!(entry.item.history_text(), "exact archived body"); + let payload = entry + .metadata + .system_input + .as_ref() + .unwrap() + .feature_payload() + .unwrap(); + let typed: SubjectBehaviorPayload = payload.decode("subject:behavior", 1).unwrap(); + assert_eq!(typed.behavior_md, ""); + assert!(typed.prompt_provenance.is_none()); + assert_eq!( + serde_json::to_value(typed).unwrap(), + json!({ + "behavior_md":"", "prompt_provenance":null, + }) + ); + } +} + +#[test] +fn current_v5_rejects_generic_input_without_metadata_instead_of_migrating_it() { + let mut missing: Value = serde_json::from_str(V4.lines().last().unwrap()).unwrap(); + missing["entry"]["metadata"] + .as_object_mut() + .unwrap() + .remove("system_input"); + let bytes = format!("{}\n", missing); + let root = tempfile::tempdir().unwrap(); + let (_, segment) = write_worker(root.path(), 5, bytes.as_bytes()); + assert!(WorkerSessionStore::new(root.path()).is_err()); + assert_eq!( + fs::read( + root.path() + .join("segments") + .join(format!("{segment}.jsonl")) + ) + .unwrap(), + bytes.as_bytes() + ); + assert_marker(root.path(), 5); +} diff --git a/crates/session-store/src/public_material.rs b/crates/session-store/src/public_material.rs index f27795928..2c7d66293 100644 --- a/crates/session-store/src/public_material.rs +++ b/crates/session-store/src/public_material.rs @@ -157,8 +157,8 @@ pub enum PublicMaterialUse { /// A fixed material catalog. Opening a new catalog observes newly appended /// records; reading a body through this catalog verifies its saved digest and /// byte length again. Missing/corrected/deleted sources are explicit failures. -/// Explicit evidence permission applies to model text, not the private payload; -/// Web display and observation instead use the producer's public projection. +/// Evidence eligibility never publishes private model text or Feature payload: +/// System inputs also require an explicit public projection for every public use. pub struct SessionPublicMaterialReader { session_id: SessionId, active_segment_id: SegmentId, @@ -828,7 +828,7 @@ enum CanonicalBody<'a> { Message { origin: &'a LoggedSessionHistoryOrigin, role: LoggedRole, - content: &'a [crate::LoggedContentPart], + content: std::borrow::Cow<'a, [crate::LoggedContentPart]>, }, ToolCall { origin: &'a LoggedSessionHistoryOrigin, @@ -904,27 +904,22 @@ fn canonical_body( if allowed == crate::InputEvidenceUse::Exclude { return None; } + let public = input.public_item()?; if let LoggedItem::Message { role: LoggedRole::System, - content, + .. } = &entry.item { return Some(CanonicalBody::Message { origin: &entry.metadata.origin, role: LoggedRole::System, - content, + content: std::borrow::Cow::Owned(vec![crate::LoggedContentPart::Text { + text: public.body.clone(), + }]), }); } return None; } - if matches!( - entry.metadata.origin, - LoggedSessionHistoryOrigin::DerivedSummary - | LoggedSessionHistoryOrigin::CompactionSummary - | LoggedSessionHistoryOrigin::JournalContext - ) { - return None; - } match &entry.item { LoggedItem::Message { role: LoggedRole::System, @@ -935,7 +930,7 @@ fn canonical_body( LoggedItem::Message { role, content } => Some(CanonicalBody::Message { origin: &entry.metadata.origin, role: *role, - content, + content: std::borrow::Cow::Borrowed(content), }), LoggedItem::ToolCall { call_id, diff --git a/crates/session-store/src/public_material/tests.rs b/crates/session-store/src/public_material/tests.rs index 0376af0bc..5bf7ce7ea 100644 --- a/crates/session-store/src/public_material/tests.rs +++ b/crates/session-store/src/public_material/tests.rs @@ -25,6 +25,27 @@ fn message(id: &str, role: LoggedRole, text: &str) -> LoggedHistoryEntry { }, ) } +fn system_context( + id: &str, + text: &str, + compaction: crate::InputCompactionUse, +) -> LoggedHistoryEntry { + let mut entry = message(id, LoggedRole::System, text); + entry.metadata.system_input = Some( + crate::SystemInputMetadata::new( + crate::SystemInputUsePolicy { + recording_material: crate::InputEvidenceUse::Exclude, + memory_evidence: crate::InputEvidenceUse::Exclude, + observation: crate::InputObservationUse::Hidden, + compaction, + }, + None, + None, + ) + .unwrap(), + ); + entry +} fn seed(session: SessionId, history: Vec) -> LogEntry { LogEntry::AnnotatedSegmentStart { ts: 1, @@ -348,7 +369,11 @@ fn yield_resume_cancel_keep_original_boundary_identity_across_compaction() { .descriptors() .cloned() .collect(); - let mut summary = message("summary", LoggedRole::User, "must not replace originals"); + let mut summary = system_context( + "summary", + "must not replace originals", + crate::InputCompactionUse::OpeningSummary, + ); summary.metadata.origin = LoggedSessionHistoryOrigin::CompactionSummary; summary.metadata.derivation = Some(crate::LoggedHistoryDerivation { sources: vec![ @@ -544,14 +569,18 @@ fn reasoning_proves_rollback_safety_but_private_and_derived_items_are_not_materi let store = FsStore::new(temp.path()).unwrap(); let session = crate::new_session_id(); let segment = crate::new_segment_id(); - let mut derived = message("derived", LoggedRole::User, "summary"); + let mut derived = system_context("derived", "summary", crate::InputCompactionUse::Retain); derived.metadata.origin = LoggedSessionHistoryOrigin::DerivedSummary; - let mut journal = message("journal", LoggedRole::User, "frozen Journal reference"); + let mut journal = system_context( + "journal", + "frozen Journal reference", + crate::InputCompactionUse::ReferenceInput, + ); journal.metadata.origin = LoggedSessionHistoryOrigin::JournalContext; - let mut summary = message( + let mut summary = system_context( "compaction-summary", - LoggedRole::User, "complementary summary", + crate::InputCompactionUse::OpeningSummary, ); summary.metadata.origin = LoggedSessionHistoryOrigin::CompactionSummary; let reasoning = entry( @@ -580,19 +609,23 @@ fn reasoning_proves_rollback_safety_but_private_and_derived_items_are_not_materi LogEntry::AnnotatedSystemItem { ts: 1, entry: crate::LoggedSystemHistoryEntry { - item: crate::SystemItem::JournalStartup { + item: crate::SystemItem::Input { body: "previous Journal must not become new recording material".into(), - snapshot: serde_json::json!({"journals": []}), - snapshot_sha256: "retained-digest".into(), - prompt_provenance: crate::PromptRenderProvenance { - workspace_id: None, - source_digest: "source".into(), - projection_digest: "projection".into(), - logical_name: "internal.subjektiv_journal_startup".into(), - }, }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + system_input: Some( + crate::SystemInputMetadata::new( + crate::SystemInputUsePolicy { + recording_material: crate::InputEvidenceUse::Exclude, + memory_evidence: crate::InputEvidenceUse::Exclude, + observation: crate::InputObservationUse::Hidden, + compaction: crate::InputCompactionUse::ReferenceInput, + }, + None, + None, + ) + .unwrap(), + ), entry_id: LoggedSessionHistoryEntryId("startup-journal".into()), origin: LoggedSessionHistoryOrigin::DerivedSummary, derivation: None, @@ -779,6 +812,19 @@ fn schema_v3_compaction_migrates_persistently_and_strict_reader_keeps_original_i panic!() }; summary.metadata.origin = LoggedSessionHistoryOrigin::CompactionSummary; + summary.metadata.system_input = Some( + crate::SystemInputMetadata::new( + crate::SystemInputUsePolicy { + recording_material: crate::InputEvidenceUse::Exclude, + memory_evidence: crate::InputEvidenceUse::Exclude, + observation: crate::InputObservationUse::Hidden, + compaction: crate::InputCompactionUse::OpeningSummary, + }, + None, + None, + ) + .unwrap(), + ); assert_eq!(history, &[summary]); // Run-transition ids synthesized from byte offsets must be frozen before // the changed summary record shifts every subsequent record offset. @@ -827,7 +873,11 @@ fn midturn_compaction_uses_exact_source_ids_and_preserves_discarded_originals() ], ) .unwrap(); - let mut summary = message("summary", LoggedRole::User, "never use as original"); + let mut summary = system_context( + "summary", + "never use as original", + crate::InputCompactionUse::OpeningSummary, + ); summary.metadata.origin = LoggedSessionHistoryOrigin::CompactionSummary; summary.metadata.derivation = Some(crate::LoggedHistoryDerivation { sources: vec![ @@ -1032,7 +1082,11 @@ fn zero_turn_fork_from_seed_and_consecutive_compactions_preserve_original_identi for index in 0..3 { let compact = crate::new_segment_id(); let summary_id = format!("summary-{index}"); - let mut summary = message(&summary_id, LoggedRole::User, "not original"); + let mut summary = system_context( + &summary_id, + "not original", + crate::InputCompactionUse::OpeningSummary, + ); summary.metadata.origin = LoggedSessionHistoryOrigin::CompactionSummary; summary.metadata.derivation = Some(crate::LoggedHistoryDerivation { sources: source_ids @@ -1180,7 +1234,11 @@ fn correction_preserves_original_identity_across_compaction_without_summary_fall ], ) .unwrap(); - let mut summary = message("summary", LoggedRole::User, "not original"); + let mut summary = system_context( + "summary", + "not original", + crate::InputCompactionUse::OpeningSummary, + ); summary.metadata.origin = LoggedSessionHistoryOrigin::CompactionSummary; summary.metadata.derivation = Some(crate::LoggedHistoryDerivation { sources: vec![LoggedSessionHistoryEntryId("result".into())], @@ -1228,7 +1286,11 @@ fn compaction_without_exact_derivation_or_with_invalid_turn_is_rejected() { ), ] { let segment = crate::new_segment_id(); - let mut summary = message("summary", LoggedRole::User, "not original"); + let mut summary = system_context( + "summary", + "not original", + crate::InputCompactionUse::OpeningSummary, + ); summary.metadata.origin = LoggedSessionHistoryOrigin::CompactionSummary; summary.metadata.derivation = derivation; let mut start = seed(session, vec![summary]); @@ -1257,54 +1319,69 @@ fn generic_system_material_uses_explicit_evidence_purpose_not_producer_or_body() let session = crate::new_session_id(); let segment = crate::new_segment_id(); let fork = crate::new_segment_id(); - let make = |producer: &str, recording_material, memory_evidence, body: &str| { - LoggedSystemHistoryEntry::from_input( - LoggedSessionHistoryOrigin::Provided { - producer: producer.into(), - }, - None, - SystemInputEnvelope::new( - body, - SystemInputMetadata::new( - SystemInputUsePolicy { - recording_material, - memory_evidence, - observation: InputObservationUse::Hidden, - compaction: InputCompactionUse::Retain, - }, - Some( - FeatureInputPayload::encode( - "unknown:future", - 99, - &serde_json::json!({"private": "not-public"}), - ) - .unwrap(), - ), - None, + let make = + |producer: &str, recording_material, memory_evidence, body: &str, public: bool| { + LoggedSystemHistoryEntry::from_input( + LoggedSessionHistoryOrigin::Provided { + producer: producer.into(), + }, + None, + SystemInputEnvelope::new( + format!("private model text: {body}"), + SystemInputMetadata::new( + SystemInputUsePolicy { + recording_material, + memory_evidence, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::Retain, + }, + Some( + FeatureInputPayload::encode( + "unknown:future", + 99, + &serde_json::json!({"private": "not-public"}), + ) + .unwrap(), + ), + public.then(|| crate::PublicSystemItem { + kind: "context".into(), + body: body.into(), + presentation: crate::PublicSystemPresentation::Context, + }), + ) + .unwrap(), ) .unwrap(), ) - .unwrap(), - ) - }; + }; let inputs = [ make( "same-owner", InputEvidenceUse::Include, InputEvidenceUse::Exclude, "do not extract this; original material", + true, ), make( "same-owner", InputEvidenceUse::Exclude, InputEvidenceUse::Include, "memory-only original", + true, ), make( "renamed-owner", InputEvidenceUse::Exclude, InputEvidenceUse::Exclude, "reference without warning text", + true, + ), + make( + "private-only", + InputEvidenceUse::Include, + InputEvidenceUse::Include, + "must not publish without projection", + false, ), ]; let root = if worker_layout { @@ -1403,6 +1480,7 @@ fn generic_system_material_uses_explicit_evidence_purpose_not_producer_or_body() assert_eq!(value["role"], "system"); assert_eq!(value["content"][0]["text"], expected_text); assert!(!body.contains("not-public")); + assert!(!body.contains("private model text")); assert!(!body.contains("unknown:future")); } } diff --git a/crates/session-store/src/system_item.rs b/crates/session-store/src/system_item.rs index 6d3d6ce43..c7bc6a3b9 100644 --- a/crates/session-store/src/system_item.rs +++ b/crates/session-store/src/system_item.rs @@ -86,23 +86,16 @@ pub struct PromptRenderProvenance { /// `notify_wrapper` template) re-shape the live rendering on a later /// resume. /// -/// New variants get added here as fresh injection kinds come online -/// (e.g. `TaskReminder`). The `kind` JSON tag is the snake_case form of -/// the variant name. +/// Feature-owned inputs use `Input` plus explicit annotated metadata; adding a +/// Feature must not extend this enum or ordinary replay. Only core events have +/// dedicated variants. Pre-v5 Feature kinds exist exclusively in the migration +/// decoder, not in this current wire contract. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum SystemItem { /// Generic System body; private metadata and public presentation are supplied /// independently by the producer in the same annotated history record. Input { body: String }, - /// One-time derived Journal reference context. The bounded Host snapshot is - /// retained as JSON independently of its rendered body and prompt projection. - JournalStartup { - body: String, - snapshot: serde_json::Value, - snapshot_sha256: String, - prompt_provenance: PromptRenderProvenance, - }, /// Free-form notification sent in by an external caller via /// `Method::Notify`. `message` is the raw caller-supplied text; /// `body` is the wrapped LLM-context form (Worker renders it via @@ -125,14 +118,6 @@ pub enum SystemItem { prompt_provenance: Option, }, - /// Result of an explicitly selected chat Feature invocation. The typed - /// result is retained for clients; `body` is the exact bounded context made - /// visible to the Worker model. - FeatureInvocationResult { - result: protocol::FeatureInvocationResult, - body: String, - }, - /// `@` file reference resolution. `body` is the rendered /// LLM-context text (`[File: ]\n…` for regular files, /// `[Dir: ]\n…` for directory listings, possibly with a @@ -140,10 +125,6 @@ pub enum SystemItem { /// byte-identical to what was sent. FileAttachment { path: String, body: String }, - /// Explicit Agent Skill activation. `body` is the exact LLM-facing - /// Skill text committed before any subsequent LLM request can use it. - SkillActivation { name: String, body: String }, - /// Historical persisted Knowledge reference resolution. Knowledge is no /// longer active, so restored sessions ignore this item instead of replaying /// archived record text into model context. @@ -167,29 +148,6 @@ pub enum SystemItem { prompt_provenance: Option, }, - /// Restore-boundary refresh of the current resident Memory projection. - /// The exact rendered body is retained so replay is byte-identical; a - /// refresh with no ready summary explicitly supersedes older resident - /// context without deleting or rewriting history. - ResidentSummaryRefresh { - body: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - prompt_provenance: Option, - }, - - /// A Host-fetched change to user-managed Subject behavior. Each changed - /// body is appended rather than rewriting prior prompt/session content. - SubjectBehaviorRefresh { - body: String, - /// Exact user-managed behavior acknowledged after durable commit. Old - /// logs omit this value; an empty value is not an acknowledgement of a - /// nonempty behavior and does not replace their stored rendered body. - #[serde(default)] - behavior_md: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - prompt_provenance: Option, - }, - /// Synthetic note inserted after an interrupted turn before the next /// user input. `body` is the exact LLM-context text explaining that the /// previous turn was cut short. @@ -233,13 +191,9 @@ impl SystemItem { Self::Input { .. } | Self::LegacyKnowledgeIgnored { .. } | Self::LegacyIgnored { .. } => PublicSystemPresentation::Ignored, - Self::JournalStartup { .. } - | Self::ResidentSummaryRefresh { .. } - | Self::SubjectBehaviorRefresh { .. } - | Self::SkillActivation { .. } - | Self::FeatureInvocationResult { .. } - | Self::FileAttachment { .. } - | Self::Interrupt { .. } => PublicSystemPresentation::Context, + Self::FileAttachment { .. } | Self::Interrupt { .. } => { + PublicSystemPresentation::Context + } }; PublicSystemItem { kind: self.kind_label().to_owned(), @@ -257,19 +211,14 @@ impl SystemItem { pub fn history_text(&self) -> String { match self { SystemItem::Input { body } => body.clone(), - SystemItem::JournalStartup { body, .. } => body.clone(), SystemItem::Notification { body, .. } => body.clone(), SystemItem::WorkerEvent { body, .. } => body.clone(), - SystemItem::FeatureInvocationResult { body, .. } => body.clone(), SystemItem::FileAttachment { body, .. } => body.clone(), - SystemItem::SkillActivation { body, .. } => body.clone(), SystemItem::LegacyKnowledgeIgnored { .. } => String::new(), SystemItem::LegacyIgnored { slug } => { format!("Ignored legacy procedure item: /{slug}") } SystemItem::TaskReminder { body, .. } => body.clone(), - SystemItem::ResidentSummaryRefresh { body, .. } => body.clone(), - SystemItem::SubjectBehaviorRefresh { body, .. } => body.clone(), SystemItem::Interrupt { body, .. } => body.clone(), } } @@ -285,17 +234,12 @@ impl SystemItem { pub fn kind_label(&self) -> &'static str { match self { SystemItem::Input { .. } => "input", - SystemItem::JournalStartup { .. } => "journal_startup", SystemItem::Notification { .. } => "notification", SystemItem::WorkerEvent { .. } => "worker_event", - SystemItem::FeatureInvocationResult { .. } => "feature_invocation_result", SystemItem::FileAttachment { .. } => "file_attachment", - SystemItem::SkillActivation { .. } => "skill_activation", SystemItem::LegacyKnowledgeIgnored { .. } => "legacy_knowledge_ignored", SystemItem::LegacyIgnored { .. } => "legacy_ignored", SystemItem::TaskReminder { .. } => "task_reminder", - SystemItem::ResidentSummaryRefresh { .. } => "resident_summary_refresh", - SystemItem::SubjectBehaviorRefresh { .. } => "subject_behavior_refresh", SystemItem::Interrupt { .. } => "interrupt", } } @@ -353,18 +297,20 @@ mod tests { } #[test] - fn subject_behavior_refresh_round_trips_with_exact_body() { - let item = SystemItem::SubjectBehaviorRefresh { - body: "current Subject context".to_string(), - behavior_md: "exact behavior".to_string(), - prompt_provenance: None, - }; - let raw = serde_json::to_string(&item).unwrap(); - let parsed: SystemItem = serde_json::from_str(&raw).unwrap(); - assert_eq!(parsed.history_text(), "current Subject context"); - assert!( - matches!(parsed, SystemItem::SubjectBehaviorRefresh { behavior_md, .. } if behavior_md == "exact behavior") - ); + fn current_system_item_rejects_frozen_feature_kinds() { + // Even well-formed old payloads cannot enter normal v5 decode/replay. + for line in include_str!("legacy_session_log/tests/fixtures/v4-system-inputs.jsonl") + .lines() + .take(5) + { + let record: serde_json::Value = serde_json::from_str(line).unwrap(); + assert!(serde_json::from_value::(record["entry"]["item"].clone()).is_err()); + assert!( + serde_json::from_value::(record["entry"].clone()) + .is_err() + ); + assert!(serde_json::from_str::(line).is_err()); + } } #[test] @@ -392,21 +338,6 @@ mod tests { assert!(serde_json::from_value::(unknown).is_err()); } - #[test] - fn frozen_subject_behavior_log_reads_old_counter_without_republishing_it() { - let old = r#"{"kind":"subject_behavior_refresh","body":"exact archived body","behavior_revision":7}"#; - let item: SystemItem = serde_json::from_str(old).unwrap(); - assert_eq!(item.history_text(), "exact archived body"); - assert_eq!( - serde_json::to_value(item).unwrap(), - serde_json::json!({ - "kind": "subject_behavior_refresh", - "body": "exact archived body", - "behavior_md": "" - }) - ); - } - #[test] fn notification_history_text_returns_stored_body() { let item = SystemItem::Notification { @@ -500,29 +431,6 @@ mod tests { } } - #[test] - fn invocation_result_retains_typed_outcome_and_exact_context() { - let result = protocol::FeatureInvocationResult { - invocation_id: "stable-invoke".into(), - identity: protocol::FeatureInvocationIdentity("builtin:test/prepare".into()), - status: protocol::FeatureInvocationStatus::OutcomeUnknown, - message: "operation may have committed".into(), - context: None, - }; - let body = "Outcome unknown; do not retry automatically"; - let item = SystemItem::FeatureInvocationResult { - result: result.clone(), - body: body.into(), - }; - let restored: SystemItem = - serde_json::from_str(&serde_json::to_string(&item).unwrap()).unwrap(); - assert_eq!(restored.history_text(), body); - assert_eq!(restored.kind_label(), "feature_invocation_result"); - assert!( - matches!(restored, SystemItem::FeatureInvocationResult { result: saved, .. } if saved == result) - ); - } - #[test] fn round_trip_worker_event() { let item = SystemItem::WorkerEvent { diff --git a/crates/session-store/src/worker_session_store.rs b/crates/session-store/src/worker_session_store.rs index bb6ffe34f..17a2691d1 100644 --- a/crates/session-store/src/worker_session_store.rs +++ b/crates/session-store/src/worker_session_store.rs @@ -36,7 +36,8 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; -const SESSION_SCHEMA_VERSION: u32 = 4; +const SESSION_SCHEMA_VERSION: u32 = 5; +const COMPACTION_SESSION_SCHEMA_VERSION: u32 = 4; const ANNOTATED_SESSION_SCHEMA_VERSION: u32 = 3; const PREVIOUS_SESSION_SCHEMA_VERSION: u32 = 2; const LEGACY_SESSION_SCHEMA_VERSION: u32 = 1; @@ -204,7 +205,15 @@ impl WorkerSessionStore { /// Open the Session store rooted at `/session`. pub fn new(root: impl Into) -> Result { - let root = root.into(); + Self::new_with_migration_writer(root.into(), atomic_write_bytes) + } + + // Keep fault injection at the storage publication boundary, without global + // flags or a public alternate restore API. Production always uses atomic writes. + fn new_with_migration_writer( + root: PathBuf, + write_segment: impl FnMut(&Path, &[u8]) -> Result<(), StoreError>, + ) -> Result { fs::create_dir_all(root.join(SEGMENTS_DIR))?; let session_id = match fs::read(root.join(SESSION_FILE)) { Ok(bytes) => { @@ -215,11 +224,13 @@ impl WorkerSessionStore { } LEGACY_SESSION_SCHEMA_VERSION | PREVIOUS_SESSION_SCHEMA_VERSION - | ANNOTATED_SESSION_SCHEMA_VERSION => { - migrate_segment_logs_to_v4( + | ANNOTATED_SESSION_SCHEMA_VERSION + | COMPACTION_SESSION_SCHEMA_VERSION => { + migrate_segment_logs_to_v5( segment_log_paths(&root)?, manifest.session_id, manifest.schema_version, + write_segment, )?; manifest.schema_version = SESSION_SCHEMA_VERSION; atomic_write_json(&root.join(SESSION_FILE), &manifest)?; @@ -1162,10 +1173,11 @@ fn segment_log_paths_in_directory( Ok(paths) } -fn migrate_segment_logs_to_v4( +fn migrate_segment_logs_to_v5( paths: Vec<(SegmentId, PathBuf)>, session_id: SessionId, source_schema_version: u32, + mut write_segment: impl FnMut(&Path, &[u8]) -> Result<(), StoreError>, ) -> Result<(), StoreError> { struct MigrationPlan { path: PathBuf, @@ -1196,12 +1208,19 @@ fn migrate_segment_logs_to_v4( } // Resolve trusted creation boundaries first, then update inherited copies by // logical identity too (fork/rewind seeds retain the same entry id). - let summaries = crate::legacy_session_log::compaction_summary_ids( - plans.iter().flat_map(|plan| plan.entries.iter()), - ); + let summaries = if source_schema_version < COMPACTION_SESSION_SCHEMA_VERSION { + crate::legacy_session_log::compaction_summary_ids( + plans.iter().flat_map(|plan| plan.entries.iter()), + ) + } else { + // v4 already made the explicit origin migration. Preserve its origins. + Default::default() + }; for plan in &mut plans { crate::legacy_session_log::migrate_compaction_origins(&mut plan.entries, &summaries); - for entry in std::mem::take(&mut plan.entries) { + for mut entry in std::mem::take(&mut plan.entries) { + crate::legacy_session_log::migrate_system_inputs(&mut entry)?; + entry.validate_system_inputs()?; serde_json::to_writer(&mut plan.output, &entry)?; plan.output.push(b'\n'); } @@ -1223,7 +1242,7 @@ fn migrate_segment_logs_to_v4( } for plan in plans { - atomic_write_bytes(&plan.path, &plan.output)?; + write_segment(&plan.path, &plan.output)?; } Ok(()) } @@ -1240,9 +1259,19 @@ pub(crate) fn migrate_fs_session(root: &Path, session_id: SessionId) -> Result<( Err(error) => return Err(error.into()), }; match version { - SESSION_SCHEMA_VERSION => Ok(()), - ANNOTATED_SESSION_SCHEMA_VERSION => { - migrate_segment_logs_to_v4(segment_log_paths_in_directory(root)?, session_id, version)?; + SESSION_SCHEMA_VERSION => { + validate_canonical_segment_paths(segment_log_paths_in_directory(root)?) + } + LEGACY_SESSION_SCHEMA_VERSION + | PREVIOUS_SESSION_SCHEMA_VERSION + | ANNOTATED_SESSION_SCHEMA_VERSION + | COMPACTION_SESSION_SCHEMA_VERSION => { + migrate_segment_logs_to_v5( + segment_log_paths_in_directory(root)?, + session_id, + version, + atomic_write_bytes, + )?; atomic_write_json(&marker, &SESSION_SCHEMA_VERSION) } _ => Err(StoreError::Corrupt { @@ -1253,8 +1282,15 @@ pub(crate) fn migrate_fs_session(root: &Path, session_id: SessionId) -> Result<( } fn validate_canonical_segment_logs(root: &Path) -> Result<(), StoreError> { - for (_, path) in segment_log_paths(root)? { - let _: Vec = parse_jsonl(&fs::read(&path)?)?; + validate_canonical_segment_paths(segment_log_paths(root)?) +} + +fn validate_canonical_segment_paths(paths: Vec<(SegmentId, PathBuf)>) -> Result<(), StoreError> { + for (_, path) in paths { + let entries: Vec = parse_jsonl(&fs::read(&path)?)?; + for entry in entries { + entry.validate_system_inputs()?; + } } Ok(()) } @@ -2022,6 +2058,146 @@ mod tests { ); } + #[test] + fn v5_migration_retries_after_second_file_publication_fails() { + fn identity_and_body(entries: &[LogEntry]) -> Vec { + entries + .iter() + .flat_map(|record| match record { + LogEntry::AnnotatedSystemItem { entry, .. } => vec![entry.to_history_entry()], + LogEntry::AnnotatedUserInput { history, .. } => history.clone(), + _ => panic!("unexpected record in fixed migration fixture"), + }) + .map(|entry| { + serde_json::json!({ + "entry_id": entry.metadata.entry_id, + "origin": entry.metadata.origin, + "derivation": entry.metadata.derivation, + "item": entry.item, + }) + }) + .collect() + } + + const SOURCE: &str = + include_str!("legacy_session_log/tests/fixtures/v4-system-inputs.jsonl"); + let root = tempfile::tempdir().unwrap(); + let session_id = new_session_id(); + let first_id = SegmentId::from_u128(1); + let second_id = SegmentId::from_u128(2); + fs::create_dir_all(root.path().join(SEGMENTS_DIR)).unwrap(); + let marker_path = root.path().join(SESSION_FILE); + atomic_write_json( + &marker_path, + &SessionManifest { + schema_version: COMPACTION_SESSION_SCHEMA_VERSION, + session_id, + }, + ) + .unwrap(); + let old_marker = fs::read(&marker_path).unwrap(); + let first_path = root + .path() + .join(SEGMENTS_DIR) + .join(format!("{first_id}.jsonl")); + let second_path = root + .path() + .join(SEGMENTS_DIR) + .join(format!("{second_id}.jsonl")); + let second_source = SOURCE.replace("-id", "-second-id"); + fs::write(&first_path, SOURCE).unwrap(); + fs::write(&second_path, &second_source).unwrap(); + let old_first = fs::read(&first_path).unwrap(); + let old_second = fs::read(&second_path).unwrap(); + let first_identity = + identity_and_body(&parse_legacy_jsonl(4, session_id, first_id, &old_first).unwrap()); + let second_identity = + identity_and_body(&parse_legacy_jsonl(4, session_id, second_id, &old_second).unwrap()); + + let mut writes = 0; + let mut first_published = None; + let failure = WorkerSessionStore::new_with_migration_writer( + root.path().to_path_buf(), + |path, bytes| { + writes += 1; + match writes { + 1 => { + assert_eq!(path, first_path.as_path()); + // Both files are still old when the first publication starts. + assert_eq!(fs::read(&first_path).unwrap(), old_first); + assert_eq!(fs::read(&second_path).unwrap(), old_second); + atomic_write_bytes(path, bytes)?; + first_published = Some(bytes.to_vec()); + Ok(()) + } + 2 => { + assert_eq!(path, second_path.as_path()); + assert_eq!( + fs::read(&first_path).unwrap(), + *first_published.as_ref().unwrap() + ); + assert_eq!(fs::read(&second_path).unwrap(), old_second); + // Fail only this atomic-write boundary, after complete + // preflight and a real successful first-file publication. + Err(std::io::Error::other("injected second migration write failure").into()) + } + _ => panic!("publication continued after its write failure"), + } + }, + ); + let error = match failure { + Ok(_) => panic!("the injected publication failure must reject store open"), + Err(error) => error, + }; + assert!( + error + .to_string() + .contains("injected second migration write failure") + ); + assert_eq!(writes, 2); + let first_published = first_published.unwrap(); + assert_ne!(first_published, old_first); + assert_eq!(fs::read(&first_path).unwrap(), first_published); + assert_eq!(fs::read(&second_path).unwrap(), old_second); + assert_eq!(fs::read(&marker_path).unwrap(), old_marker); + let updated_first: Vec = parse_jsonl(&first_published).unwrap(); + for entry in &updated_first { + entry.validate_system_inputs().unwrap(); + } + assert!(matches!( + &updated_first[0], + LogEntry::AnnotatedSystemItem { + entry: crate::LoggedSystemHistoryEntry { + item: crate::SystemItem::Input { .. }, + .. + }, + .. + } + )); + assert_eq!(identity_and_body(&updated_first), first_identity); + + // Retry via the public production constructor, without any injection or + // hand-built mixed fixture. The old marker drives the same migration. + let store = WorkerSessionStore::new(root.path()).unwrap(); + let first = store.read_all(session_id, first_id).unwrap(); + let second = store.read_all(session_id, second_id).unwrap(); + assert_eq!(identity_and_body(&first), first_identity); + assert_eq!(identity_and_body(&second), second_identity); + for entry in first.iter().chain(second.iter()) { + entry.validate_system_inputs().unwrap(); + } + assert_eq!(fs::read(&first_path).unwrap(), first_published); + let second_published = fs::read(&second_path).unwrap(); + assert_ne!(second_published, old_second); + let manifest: SessionManifest = + serde_json::from_slice(&fs::read(&marker_path).unwrap()).unwrap(); + assert_eq!(manifest.schema_version, SESSION_SCHEMA_VERSION); + drop(store); + WorkerSessionStore::new(root.path()).unwrap(); + assert_eq!(fs::read(&first_path).unwrap(), first_published); + assert_eq!(fs::read(&second_path).unwrap(), second_published); + } + #[test] fn current_jsonl_requires_annotations_across_append_rewrite_and_reopen() { let root = tempfile::tempdir().unwrap(); diff --git a/crates/standalone/src/subjektiv/journal/tests/handoff.rs b/crates/standalone/src/subjektiv/journal/tests/handoff.rs index 4b571a313..03e5b6ebf 100644 --- a/crates/standalone/src/subjektiv/journal/tests/handoff.rs +++ b/crates/standalone/src/subjektiv/journal/tests/handoff.rs @@ -79,7 +79,7 @@ async fn recorded_idle_body_handoff_preserves_open_after_source_deletion_and_see let sessions = session_store::FsStore::new_with_retention(workers.sessions_dir(second.worker_id()), crate::store::session_retention(&state)).unwrap(); let pointer = session_store::FsWorkerStore::new(workers.worker_metadata_dir(second.worker_id())).unwrap().read_by_name(&second.record().storage_key).unwrap().unwrap().active.unwrap(); let entries = sessions.read_all(session, pointer.segment_id.unwrap()).unwrap(); - let journal_items = entries.iter().filter(|entry| matches!(entry, session_store::LogEntry::AnnotatedSystemItem { entry, .. } if matches!(entry.item, session_store::SystemItem::JournalStartup { .. }))).count(); + let journal_items = entries.iter().filter(|entry| matches!(entry, session_store::LogEntry::AnnotatedSystemItem { entry, .. } if entry.metadata.system_input.as_ref().and_then(|input| input.feature_payload()).is_some_and(|payload| payload.schema() == "journal:startup"))).count(); assert_eq!(journal_items, 1); assert_eq!(catalog.store.subject(&subject.id).unwrap().unwrap().behavior_md, "BEHAVIOR_ORIGINAL_UNCHANGED"); second.shutdown().await.unwrap(); diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index d8be6da1a..f9d7f240f 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -1656,10 +1656,10 @@ where if ordinary_subjektiv_features_enabled && lifecycle_enabled && let Some(host) = subjektiv_host.as_ref() - && host.host.journal_startup_granted() + && (host.host.journal_startup_granted() || host.host.journal_compaction_granted()) { - worker.install_initial_context_source(Arc::new( - crate::feature::builtin::journal::JournalStartupContextSource::new( + worker.install_session_context_source(Arc::new( + crate::feature::builtin::journal::JournalContextSource::new( host.clone(), worker.prompts(), ), diff --git a/crates/worker/src/feature/builtin/journal.rs b/crates/worker/src/feature/builtin/journal.rs index 990cd347a..15d2bb2b4 100644 --- a/crates/worker/src/feature/builtin/journal.rs +++ b/crates/worker/src/feature/builtin/journal.rs @@ -23,7 +23,7 @@ use crate::hook::{ use crate::subjektiv::{SubjektivHostConnection, SubjektivHostError}; use agen::tool::{Tool, ToolDefinition, ToolError, ToolExecutionContext, ToolMeta, ToolOutput}; use async_trait::async_trait; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::sync::Arc; use subjektiv::journal::transport::*; @@ -32,12 +32,73 @@ use subjektiv::journal::{ MAX_JOURNAL_RESPONSE_BYTES, }; +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct JournalStartupInput { + pub snapshot: subjektiv::journal::startup::JournalStartupSnapshot, + pub snapshot_sha256: String, + pub prompt_provenance: session_store::PromptRenderProvenance, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum JournalReadStatus { + Available, + Absent, + Disabled, + Unavailable, +} +impl JournalReadStatus { + fn as_str(self) -> &'static str { + match self { + Self::Available => "available", + Self::Absent => "absent", + Self::Disabled => "disabled", + Self::Unavailable => "unavailable", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct JournalCompactionInput { + status: JournalReadStatus, + snapshot: Option, + snapshot_sha256: Option, + prompt_provenance: session_store::PromptRenderProvenance, +} + +fn journal_input( + body: String, + payload: session_store::FeatureInputPayload, + kind: &str, +) -> Result { + use session_store::*; + let public = PublicSystemItem { + kind: kind.into(), + body: body.clone(), + presentation: PublicSystemPresentation::Context, + }; + let metadata = SystemInputMetadata::new( + SystemInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::ReferenceInput, + }, + Some(payload), + Some(public), + ) + .map_err(|e| e.to_string())?; + SystemInputEnvelope::new(body, metadata).map_err(|e| e.to_string()) +} + /// Subjektiv owns startup projection; Core only commits its typed initial items. -pub(crate) struct JournalStartupContextSource { +pub(crate) struct JournalContextSource { host: SubjektivHostConnection, prompts: Arc>, } -impl JournalStartupContextSource { +impl JournalContextSource { pub(crate) fn new( host: SubjektivHostConnection, prompts: Arc>, @@ -46,9 +107,12 @@ impl JournalStartupContextSource { } } #[async_trait] -impl crate::worker::InitialContextSource for JournalStartupContextSource { +impl crate::worker::SessionContextSource for JournalContextSource { async fn load(&self) -> Result, String> { use sha2::{Digest, Sha256}; + if !self.host.host.journal_startup_granted() { + return Ok(Vec::new()); + } let snapshot = self .host .journal_startup_context() @@ -58,8 +122,8 @@ impl crate::worker::InitialContextSource for JournalStartupContextSource { let empty = snapshot.journals.is_empty() && !snapshot.omitted; // Hash the same canonical JSON value retained by SystemItem, so replay // can verify provenance without depending on domain struct field order. - let snapshot = serde_json::to_value(snapshot).map_err(|e| e.to_string())?; - let snapshot_json = serde_json::to_string(&snapshot).map_err(|e| e.to_string())?; + let snapshot_value = serde_json::to_value(&snapshot).map_err(|e| e.to_string())?; + let snapshot_json = serde_json::to_string(&snapshot_value).map_err(|e| e.to_string())?; if snapshot_json.len() > subjektiv::journal::startup::STARTUP_MAX_BYTES { return Err("Host Journal startup snapshot exceeds independent byte budget".into()); } @@ -74,9 +138,10 @@ impl crate::worker::InitialContextSource for JournalStartupContextSource { return Err("Journal startup prompt exceeds rendered byte budget".into()); } let projection = prompts.projection(); - Ok(vec![session_store::LoggedSystemHistoryEntry { - item: session_store::SystemItem::JournalStartup { - body, + let payload = session_store::FeatureInputPayload::encode( + "journal:startup", + 1, + &JournalStartupInput { snapshot, snapshot_sha256: Sha256::digest(snapshot_json.as_bytes()) .iter() @@ -89,11 +154,38 @@ impl crate::worker::InitialContextSource for JournalStartupContextSource { logical_name: "internal.subjektiv_journal_startup".into(), }, }, - metadata: crate::session_history::metadata( - session_store::LoggedSessionHistoryOrigin::JournalContext, - None, - ), - }]) + ) + .map_err(|e| e.to_string())?; + Ok(vec![session_store::LoggedSystemHistoryEntry::from_input( + session_store::LoggedSessionHistoryOrigin::Provided { + producer: "subjektiv.journal".into(), + }, + None, + journal_input(body, payload, "journal_startup")?, + )]) + } + + async fn load_compaction_context( + &self, + session: session_store::SessionId, + ) -> Result, String> { + // A changed Session must never reuse the old source's Host ownership. + let connection = if self.host.context.session_id == session.to_string() { + Ok(Some(self.host.clone())) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "context source belongs to a different Session", + )) + }; + let input = freeze_compaction_journal(connection, &self.prompts.load_full())?; + Ok(vec![session_store::LoggedSystemHistoryEntry::from_input( + session_store::LoggedSessionHistoryOrigin::Provided { + producer: "subjektiv.journal".into(), + }, + None, + input, + )]) } } @@ -103,7 +195,8 @@ impl crate::worker::InitialContextSource for JournalStartupContextSource { pub(crate) fn freeze_compaction_journal( connection: std::io::Result>, prompts: &crate::prompt::catalog::PromptCatalog, -) -> Result { +) -> Result { + use JournalReadStatus::*; let workspace_id = connection.as_ref().ok().and_then(|connection| { connection .as_ref() @@ -116,24 +209,25 @@ pub(crate) fn freeze_compaction_journal( Ok(_) => None, Err(_) => Some(Err(())), }; + let snapshot = read.as_ref().and_then(|read| read.as_ref().ok()).cloned(); let (status, json) = match read { - None => ("disabled", String::new()), + None => (Disabled, String::new()), Some(Ok(snapshot)) => { let status = if snapshot.journals.is_empty() { - "absent" + Absent } else { - "available" + Available }; match serde_json::to_string(&snapshot) { Ok(json) if json.len() <= subjektiv::journal::startup::STARTUP_MAX_BYTES => { (status, json) } - _ => ("unavailable", String::new()), + _ => (Unavailable, String::new()), } } - Some(Err(())) => ("unavailable", String::new()), + Some(Err(())) => (Unavailable, String::new()), }; - if status == "unavailable" { + if status == Unavailable { tracing::warn!("Journal compaction snapshot unavailable; continuing without Journal"); } use sha2::{Digest, Sha256}; @@ -143,12 +237,35 @@ pub(crate) fn freeze_compaction_journal( .map(|byte| format!("{byte:02x}")) .collect() }); - prompts.subjektiv_journal_compaction( - status, - &json, - snapshot_sha256.as_deref(), - workspace_id.as_deref(), + let body = prompts + .subjektiv_journal_compaction( + status.as_str(), + &json, + snapshot_sha256.as_deref(), + workspace_id.as_deref(), + ) + .map_err(|e| e.to_string())?; + let payload = session_store::FeatureInputPayload::encode( + "journal:compaction", + 1, + &JournalCompactionInput { + status, + prompt_provenance: session_store::PromptRenderProvenance { + workspace_id, + source_digest: prompts.projection().source_digest.clone(), + projection_digest: prompts.projection().catalog_digest.clone(), + logical_name: "internal.subjektiv_journal_compaction".into(), + }, + snapshot: if status == Unavailable { + None + } else { + snapshot + }, + snapshot_sha256, + }, ) + .map_err(|e| e.to_string())?; + journal_input(body, payload, "journal_compaction") } #[derive(Clone)] diff --git a/crates/worker/src/feature/builtin/memory.rs b/crates/worker/src/feature/builtin/memory.rs index 32155ea04..f6f5869e6 100644 --- a/crates/worker/src/feature/builtin/memory.rs +++ b/crates/worker/src/feature/builtin/memory.rs @@ -43,6 +43,80 @@ use crate::worker::{ WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceRequestMethod, }; +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct SubjectBehaviorInput { + pub behavior_md: String, + pub prompt_provenance: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct ResidentSummaryInput { + prompt_provenance: Option, +} + +fn resident_input( + body: String, + schema: &str, + payload: &impl serde::Serialize, + kind: &str, +) -> Result { + use session_store::*; + let input = SystemInputEnvelope::new( + body.clone(), + SystemInputMetadata::new( + SystemInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::Retain, + }, + Some(FeatureInputPayload::encode(schema, 1, payload)?), + Some(PublicSystemItem { + kind: kind.into(), + body, + presentation: PublicSystemPresentation::Context, + }), + )?, + )?; + Ok(LoggedSystemHistoryEntry::from_input( + LoggedSessionHistoryOrigin::Provided { + producer: "subjektiv.resident-context".into(), + }, + None, + input, + )) +} + +pub(crate) fn subject_behavior_input( + body: String, + behavior_md: String, + prompt_provenance: Option, +) -> Result { + resident_input( + body, + "subject:behavior", + &SubjectBehaviorInput { + behavior_md, + prompt_provenance, + }, + "subject_behavior_refresh", + ) +} + +pub(crate) fn resident_summary_input( + body: String, + prompt_provenance: Option, +) -> Result { + resident_input( + body, + "memory:resident-summary", + &ResidentSummaryInput { prompt_provenance }, + "resident_summary_refresh", + ) +} + #[derive(Clone, Debug)] pub struct WorkspaceHttpMemoryBackend { client: Arc, @@ -714,7 +788,7 @@ impl WorkspaceSubjektivResidentSummarySource { fn render( &self, output: &server_api::SubjektivResidentContextOutput, - ) -> Result { + ) -> Result<(String, session_store::PromptRenderProvenance), String> { use memory::backend::MemoryResidentSummaryAvailability as Availability; let availability = match output.memory_surface.availability { Availability::Ready => "ready", @@ -723,14 +797,24 @@ impl WorkspaceSubjektivResidentSummarySource { Availability::Failed => "failed", Availability::Unavailable => "unavailable", }; - self.prompts - .load() + let prompts = self.prompts.load_full(); + let body = prompts .subjektiv_resident_context( &output.behavior_md, availability, output.memory_surface.content.as_deref(), ) - .map_err(|error| error.to_string()) + .map_err(|error| error.to_string())?; + let projection = prompts.projection(); + Ok(( + body, + session_store::PromptRenderProvenance { + workspace_id: self.host.host.workspace_id(), + source_digest: projection.source_digest.clone(), + projection_digest: projection.catalog_digest.clone(), + logical_name: "internal.subjektiv_resident_context".into(), + }, + )) } fn remember_behavior(&self, output: &server_api::SubjektivResidentContextOutput) { @@ -752,7 +836,7 @@ impl SystemPromptContributionSource for WorkspaceSubjektivResidentSummarySource async fn load(&self) -> SystemPromptContribution { match self.fetch().await { Ok(output) => match self.render(&output) { - Ok(rendered) => { + Ok((rendered, _)) => { self.remember_behavior(&output); SystemPromptContribution::Ready(rendered) } @@ -788,20 +872,30 @@ impl SystemPromptContributionSource for WorkspaceSubjektivResidentSummarySource if !changed { return Ok(None); } - let rendered = self.render(&output)?; + let (rendered, prompt_provenance) = self.render(&output)?; Ok(Some(ResidentContextRefresh { body: rendered, behavior_md: output.behavior_md, + prompt_provenance: Some(prompt_provenance), })) } fn system_item_committed( &self, - item: &session_store::SystemItem, - _metadata: &crate::session_history::SessionHistoryMetadata, + _item: &session_store::SystemItem, + metadata: &crate::session_history::SessionHistoryMetadata, ) { - if let session_store::SystemItem::SubjectBehaviorRefresh { behavior_md, .. } = item { - self.confirm_resident_context_behavior(behavior_md); + if let Some(input) = metadata + .system_input + .as_ref() + .and_then(|input| input.feature_payload()) + .and_then(|payload| { + payload + .decode::("subject:behavior", 1) + .ok() + }) + { + self.confirm_resident_context_behavior(&input.behavior_md); } } @@ -954,7 +1048,13 @@ impl Hook for SubjektivBehaviorRefreshHook { "Subject behavior refresh requires durable session append authority", ) })?; - system_items.append_subject_behavior_refresh(refresh.body, refresh.behavior_md); + let input = subject_behavior_input( + refresh.body, + refresh.behavior_md, + refresh.prompt_provenance, + ) + .map_err(|error| HookError::new(HookErrorCategory::Dependency, error.to_string()))?; + system_items.append_input(input); } Ok(HookPreRequestAction::Continue) } @@ -979,16 +1079,18 @@ impl Hook for SubjektivResidentRestoreRefreshHook { .resident_memory_restore_section(surface_ready, summary) .map_err(|error| HookError::new(HookErrorCategory::Internal, error.to_string()))?; let projection = prompts.projection(); - context.system_items().append_resident_summary_refresh( + let input = resident_summary_input( body, - session_store::PromptRenderProvenance { + Some(session_store::PromptRenderProvenance { workspace_id: self.workspace_id.clone(), source_digest: projection.source_digest.clone(), projection_digest: projection.catalog_digest.clone(), logical_name: WorkerPrompt::ResidentMemoryRestoreSection.key().to_string(), - }, - ); + }), + ) + .map_err(|error| HookError::new(HookErrorCategory::Dependency, error.to_string()))?; + context.system_items().append_input(input); Ok(()) } } @@ -1751,14 +1853,13 @@ permission = "write" .expect("changed behavior must produce one refresh"); assert_eq!(refresh.behavior_md, "Ask when uncertain."); assert!(refresh.body.contains("Ask when uncertain.")); - source.system_item_committed( - &session_store::SystemItem::SubjectBehaviorRefresh { - body: refresh.body.clone(), - behavior_md: refresh.behavior_md.clone(), - prompt_provenance: None, - }, - &session_store::LoggedSessionHistoryMetadata::legacy_unknown(), - ); + let input = subject_behavior_input( + refresh.body.clone(), + refresh.behavior_md.clone(), + refresh.prompt_provenance.clone(), + ) + .unwrap(); + source.system_item_committed(&input.item, &input.metadata); assert!( source .load_changed_resident_context() @@ -1775,14 +1876,13 @@ permission = "write" .expect("cleared behavior must produce one refresh"); assert_eq!(cleared.behavior_md, ""); assert!(cleared.body.contains("No user-managed behavior is set")); - source.system_item_committed( - &session_store::SystemItem::SubjectBehaviorRefresh { - body: cleared.body.clone(), - behavior_md: cleared.behavior_md.clone(), - prompt_provenance: None, - }, - &session_store::LoggedSessionHistoryMetadata::legacy_unknown(), - ); + let input = subject_behavior_input( + cleared.body.clone(), + cleared.behavior_md.clone(), + cleared.prompt_provenance.clone(), + ) + .unwrap(); + source.system_item_committed(&input.item, &input.metadata); assert!( source .load_changed_resident_context() @@ -1840,14 +1940,13 @@ permission = "write" .await .unwrap() .unwrap(); - source.system_item_committed( - &session_store::SystemItem::SubjectBehaviorRefresh { - body: first.body.clone(), - behavior_md: first.behavior_md.clone(), - prompt_provenance: None, - }, - &session_store::LoggedSessionHistoryMetadata::legacy_unknown(), - ); + let input = subject_behavior_input( + first.body.clone(), + first.behavior_md.clone(), + first.prompt_provenance.clone(), + ) + .unwrap(); + source.system_item_committed(&input.item, &input.metadata); assert_eq!( source .load_changed_resident_context() @@ -1856,14 +1955,13 @@ permission = "write" .unwrap(), second ); - source.system_item_committed( - &session_store::SystemItem::SubjectBehaviorRefresh { - body: second.body.clone(), - behavior_md: second.behavior_md.clone(), - prompt_provenance: None, - }, - &session_store::LoggedSessionHistoryMetadata::legacy_unknown(), - ); + let input = subject_behavior_input( + second.body.clone(), + second.behavior_md.clone(), + second.prompt_provenance.clone(), + ) + .unwrap(); + source.system_item_committed(&input.item, &input.metadata); assert!( source .load_changed_resident_context() diff --git a/crates/worker/src/feature/builtin/task/mod.rs b/crates/worker/src/feature/builtin/task/mod.rs index e45b1b703..0d82bb437 100644 --- a/crates/worker/src/feature/builtin/task/mod.rs +++ b/crates/worker/src/feature/builtin/task/mod.rs @@ -276,7 +276,9 @@ mod tests { use super::*; use crate::hook::{PreRequestInfo, SystemItemAppendHandle}; - fn pre_request_context(pending: Arc>>) -> PreRequestContext { + fn pre_request_context( + pending: Arc>>, + ) -> PreRequestContext { PreRequestContext::new( PreRequestInfo { item_count: 1, @@ -315,7 +317,7 @@ mod tests { let queued = pending.lock().expect("pending queue poisoned"); assert_eq!(queued.len(), 1); - let SystemItem::TaskReminder { body, .. } = &queued[0] else { + let SystemItem::TaskReminder { body, .. } = &queued[0].item else { panic!("unexpected system item: {:?}", queued[0]); }; assert!(body.starts_with("Current session steps are listed below.")); @@ -339,7 +341,7 @@ mod tests { } let queued = pending.lock().expect("pending queue poisoned"); - let SystemItem::TaskReminder { source, body, .. } = &queued[0] else { + let SystemItem::TaskReminder { source, body, .. } = &queued[0].item else { panic!("unexpected system item: {:?}", queued[0]); }; assert_eq!(*source, SystemReminderSource::TaskInactivity); @@ -452,7 +454,7 @@ mod tests { } let queued = pending.lock().expect("pending queue poisoned"); assert_eq!(queued.len(), 1); - let SystemItem::TaskReminder { body, .. } = &queued[0] else { + let SystemItem::TaskReminder { body, .. } = &queued[0].item else { panic!("unexpected system item: {:?}", queued[0]); }; assert!(body.contains("taskid 1 (completed) done")); diff --git a/crates/worker/src/feature/invocation.rs b/crates/worker/src/feature/invocation.rs index de8bc6fbe..91009461e 100644 --- a/crates/worker/src/feature/invocation.rs +++ b/crates/worker/src/feature/invocation.rs @@ -39,6 +39,45 @@ const MAX_INVOCATION_DESCRIPTOR_BYTES: usize = protocol::invocation::MAX_FEATURE_INVOCATION_DESCRIPTOR_BYTES; const MAX_COMPLETION_RESPONSE_BYTES: usize = 64 * 1024; +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct InvocationResultInput { + pub result: FeatureInvocationResult, +} + +pub(crate) fn result_input( + result: FeatureInvocationResult, + body: String, +) -> Result { + use session_store::*; + let payload = FeatureInputPayload::encode( + "feature:invocation-result", + 1, + &InvocationResultInput { result }, + )?; + let metadata = SystemInputMetadata::new( + SystemInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::Retain, + }, + Some(payload), + Some(PublicSystemItem { + kind: "feature_invocation_result".into(), + body: body.clone(), + presentation: PublicSystemPresentation::Context, + }), + )?; + Ok(LoggedSystemHistoryEntry::from_input( + LoggedSessionHistoryOrigin::Provided { + producer: "feature.invocation".into(), + }, + None, + SystemInputEnvelope::new(body, metadata)?, + )) +} + pub(crate) fn invocation_result_is_bounded(result: &FeatureInvocationResult) -> bool { result.message.len() <= MAX_INVOCATION_MESSAGE_BYTES && result diff --git a/crates/worker/src/hook.rs b/crates/worker/src/hook.rs index a433bb7e3..c480471dc 100644 --- a/crates/worker/src/hook.rs +++ b/crates/worker/src/hook.rs @@ -25,7 +25,9 @@ use agen::tool::{ToolOutput, ToolResult}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use serde_json::Value; -use session_store::{SystemItem, SystemReminder}; +#[cfg(test)] +use session_store::SystemItem; +use session_store::SystemReminder; use thiserror::Error; use crate::SessionEntryRef; @@ -254,7 +256,7 @@ impl From for TurnEndAction { // Hook context handles // ============================================================================= -/// Host-created handle for appending approved durable [`SystemItem`] requests. +/// Host-created handle for appending approved durable [`session_store::SystemItem`] requests. /// /// Hook code can use this handle only when the Worker host includes it in an /// event-specific context. The handle queues typed requests; the host drains the @@ -263,11 +265,11 @@ impl From for TurnEndAction { /// raw `agen::Item`, history writer, event sender, `Worker`, `Engine`, or /// notification buffer. pub struct SystemItemAppendHandle { - pending: Arc>>, + pending: Arc>>, } impl SystemItemAppendHandle { - pub(crate) fn new(pending: Arc>>) -> Self { + pub(crate) fn new(pending: Arc>>) -> Self { Self { pending } } @@ -279,53 +281,44 @@ impl SystemItemAppendHandle { self.pending .lock() .expect("system-item append queue poisoned") - .push(item); + .push(session_store::LoggedSystemHistoryEntry { + item, + metadata: crate::session_history::metadata( + session_store::LoggedSessionHistoryOrigin::BackendInstruction { + operation_id: None, + }, + None, + ), + }); } - /// Queue a Subject behavior refresh with its exact represented behavior. The caller receives this - /// narrow operation only at a pre-request boundary, and the host commits it - /// before exposing it to the model. - pub fn append_subject_behavior_refresh( - &self, - body: impl Into, - behavior_md: impl Into, - ) { + /// Queue producer-validated System context. Metadata and body are committed together. + /// The Host exposes this capability only at a pre-request boundary. + pub fn append_input(&self, input: session_store::LoggedSystemHistoryEntry) { self.pending .lock() .expect("system-item append queue poisoned") - .push(SystemItem::SubjectBehaviorRefresh { - body: body.into(), - behavior_md: behavior_md.into(), - prompt_provenance: None, - }); + .push(input); } } /// Host-created narrow append authority exposed only at a restore lifecycle -/// boundary. It cannot emit arbitrary system items. +/// boundary. No model-facing tool receives this capability. pub struct RestoreSystemItemAppendHandle { - pending: Arc>>, + pending: Arc>>, } impl RestoreSystemItemAppendHandle { - pub(crate) fn new(pending: Arc>>) -> Self { + pub(crate) fn new(pending: Arc>>) -> Self { Self { pending } } - /// Queue a Feature-rendered resident-summary refresh for durable append at - /// the restore lifecycle boundary. - pub fn append_resident_summary_refresh( - &self, - body: impl Into, - prompt_provenance: session_store::PromptRenderProvenance, - ) { + /// Queue producer-validated System context at the Host restore boundary. + pub fn append_input(&self, input: session_store::LoggedSystemHistoryEntry) { self.pending .lock() .expect("restore system-item append queue poisoned") - .push(SystemItem::ResidentSummaryRefresh { - body: body.into(), - prompt_provenance: Some(prompt_provenance), - }); + .push(input); } } @@ -983,7 +976,7 @@ mod tests { let queued = pending.lock().expect("pending queue poisoned"); assert_eq!(queued.len(), 1); - match &queued[0] { + match &queued[0].item { SystemItem::TaskReminder { body, .. } => { assert_eq!(body, "remember tasks"); } @@ -992,7 +985,7 @@ mod tests { } #[test] - fn restore_append_handle_queues_only_resident_refresh_items() { + fn restore_append_handle_preserves_producer_metadata() { let pending = Arc::new(Mutex::new(Vec::new())); let handle = RestoreSystemItemAppendHandle::new(Arc::clone(&pending)); let provenance = session_store::PromptRenderProvenance { @@ -1003,15 +996,16 @@ mod tests { logical_name: "internal.resident_memory_restore_section".into(), }; - handle.append_resident_summary_refresh("current surface", provenance.clone()); - - assert!(matches!( - pending.lock().unwrap().as_slice(), - [SystemItem::ResidentSummaryRefresh { - body, - prompt_provenance: Some(actual), - }] if body == "current surface" && actual == &provenance - )); + let input = crate::feature::builtin::memory::resident_summary_input( + "current surface".into(), + Some(provenance), + ) + .unwrap(); + let expected = serde_json::to_value(&input).unwrap(); + handle.append_input(input); + let queued = pending.lock().unwrap(); + assert_eq!(queued.len(), 1); + assert_eq!(serde_json::to_value(&queued[0]).unwrap(), expected); } #[test] diff --git a/crates/worker/src/ipc/interceptor.rs b/crates/worker/src/ipc/interceptor.rs index cbb500796..1e91f1797 100644 --- a/crates/worker/src/ipc/interceptor.rs +++ b/crates/worker/src/ipc/interceptor.rs @@ -262,6 +262,31 @@ impl WorkerInterceptor { ) } + fn commit_system_entries( + &self, + entries: &[session_store::LoggedSystemHistoryEntry], + ) -> Result<(), session_store::StoreError> { + let Some(writer) = &self.log_writer else { + return Ok(()); + }; + for entry in entries { + writer.commit_annotated_system_item_then( + entry.clone(), + Vec::new(), + &mut |history| { + self.pending_committed_history + .lock() + .expect("pending committed history poisoned") + .push_back(history.clone()); + if let Some(source) = &self.resident_context_source { + source.system_item_committed(&entry.item, &history.annotation); + } + }, + )?; + } + Ok(()) + } + fn current_turn_index(&self) -> usize { self.next_turn_index .load(Ordering::Relaxed) @@ -387,11 +412,6 @@ impl WorkerInterceptor { } if prompt_provenance.is_none() => { *prompt_provenance = Some(provenance("internal.task_reminder")); } - SystemItem::SubjectBehaviorRefresh { - prompt_provenance, .. - } if prompt_provenance.is_none() => { - *prompt_provenance = Some(provenance("internal.subjektiv_resident_context")); - } SystemItem::Interrupt { prompt_provenance, .. } if prompt_provenance.is_none() => { @@ -617,15 +637,17 @@ impl Interceptor for WorkerInterceptor { return Ok(PreRequestAction::Yield); } - let mut system_items: Vec = std::mem::take( + let mut system_items: Vec = std::mem::take( &mut *pending_hook_system_items .lock() .expect("pending hook system-item queue poisoned"), ); - self.attach_prompt_provenance(&mut system_items); + for entry in &mut system_items { + self.attach_prompt_provenance(std::slice::from_mut(&mut entry.item)); + } let mut appended_items: Vec = system_items .iter() - .map(SystemItem::to_history_item) + .map(|entry| entry.item.to_history_item()) .collect(); let effective_context = if appended_items.is_empty() { Cow::Borrowed(context.as_slice()) @@ -639,7 +661,7 @@ impl Interceptor for WorkerInterceptor { let compact_decision = self.request_compact_decision(current_tokens, effective_context.as_ref()); if !matches!(compact_decision, AutomaticCompactDecision::Continue) { - if let Err(error) = self.commit_system_items(&system_items) { + if let Err(error) = self.commit_system_entries(&system_items) { return Ok(PreRequestAction::Cancel(format!( "session persistence failed: {error}" ))); @@ -683,7 +705,7 @@ impl Interceptor for WorkerInterceptor { if appended_items.is_empty() { return Ok(PreRequestAction::Continue); } - Ok(match self.commit_system_items(&system_items) { + Ok(match self.commit_system_entries(&system_items) { Ok(()) => PreRequestAction::ContinueWith(appended_items), Err(error) => PreRequestAction::Cancel(format!("session persistence failed: {error}")), }) @@ -983,7 +1005,14 @@ mod tests { input .system_items() .expect("test interceptor has durable append authority") - .append_subject_behavior_refresh("current Subject context", "Ask when uncertain."); + .append_input( + crate::feature::builtin::memory::subject_behavior_input( + "current Subject context".into(), + "Ask when uncertain.".into(), + None, + ) + .unwrap(), + ); Ok(HookPreRequestAction::Continue) } } @@ -1000,11 +1029,23 @@ mod tests { fn system_item_committed( &self, - item: &SystemItem, - _metadata: &crate::session_history::SessionHistoryMetadata, + _item: &SystemItem, + metadata: &crate::session_history::SessionHistoryMetadata, ) { - if let SystemItem::SubjectBehaviorRefresh { behavior_md, .. } = item { - self.confirmed.lock().unwrap().push(behavior_md.to_owned()); + if let Some(input) = metadata + .system_input + .as_ref() + .and_then(|input| input.feature_payload()) + .and_then(|payload| { + payload + .decode::( + "subject:behavior", + 1, + ) + .ok() + }) + { + self.confirmed.lock().unwrap().push(input.behavior_md); } } } @@ -1549,11 +1590,7 @@ mod tests { assert_eq!(*confirmed.lock().unwrap(), ["Ask when uncertain."]); assert!(matches!( committed.lock().unwrap().as_slice(), - [SystemItem::SubjectBehaviorRefresh { - behavior_md, - prompt_provenance: Some(_), - .. - }] if behavior_md == "Ask when uncertain." + [SystemItem::Input { body }] if body == "current Subject context" )); } @@ -1654,12 +1691,15 @@ mod tests { })), ) .with_resident_context_source(Some(source)); - let items = ["raw behavior", ""].map(|behavior_md| SystemItem::SubjectBehaviorRefresh { - body: "same rendered context".into(), - behavior_md: behavior_md.into(), - prompt_provenance: None, + let items = ["raw behavior", ""].map(|behavior_md| { + crate::feature::builtin::memory::subject_behavior_input( + "same rendered context".into(), + behavior_md.into(), + None, + ) + .unwrap() }); - interceptor.commit_system_items(&items).unwrap(); + interceptor.commit_system_entries(&items).unwrap(); assert_eq!(*confirmed.lock().unwrap(), ["raw behavior", ""]); assert_eq!(committed.lock().unwrap().len(), 2); } diff --git a/crates/worker/src/ipc/protocol_session.rs b/crates/worker/src/ipc/protocol_session.rs index 038770284..8fc4a5d97 100644 --- a/crates/worker/src/ipc/protocol_session.rs +++ b/crates/worker/src/ipc/protocol_session.rs @@ -340,22 +340,13 @@ mod tests { #[test] fn system_item_log_entry_keeps_identity_in_live_event() { let entry_id = session_store::LoggedSessionHistoryEntryId::new(); + let mut input = + crate::feature::builtin::memory::resident_summary_input("refresh".into(), None) + .unwrap(); + input.metadata.entry_id = entry_id.clone(); let event = live_log_entry_event(LogEntry::AnnotatedSystemItem { ts: session_store::segment_log::now_millis(), - entry: session_store::LoggedSystemHistoryEntry { - item: session_store::SystemItem::ResidentSummaryRefresh { - body: "refresh".to_string(), - prompt_provenance: None, - }, - metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, - entry_id: entry_id.clone(), - origin: session_store::LoggedSessionHistoryOrigin::BackendInstruction { - operation_id: None, - }, - derivation: None, - }, - }, + entry: input, extensions: Vec::new(), }) .expect("SystemItem must be live-relevant"); @@ -381,25 +372,22 @@ mod tests { logical_name: "private-template".into(), }; let items = [ - session_store::SystemItem::SubjectBehaviorRefresh { - body: "displayed context".into(), - behavior_md: "private-raw-behavior".into(), - prompt_provenance: Some(provenance.clone()), - }, - session_store::SystemItem::JournalStartup { - body: "displayed reference".into(), - snapshot: serde_json::json!({ "internal": "private-snapshot" }), - snapshot_sha256: "private-snapshot-digest".into(), - prompt_provenance: provenance, - }, + crate::feature::builtin::memory::subject_behavior_input( + "displayed context".into(), + "private-raw-behavior".into(), + Some(provenance.clone()), + ) + .unwrap(), + crate::feature::builtin::memory::resident_summary_input( + "displayed reference".into(), + Some(provenance), + ) + .unwrap(), ]; - for item in items { + for input in items { let entry = LogEntry::AnnotatedSystemItem { ts: 7, - entry: session_store::LoggedSystemHistoryEntry { - item, - metadata: session_store::LoggedSessionHistoryMetadata::legacy_unknown(), - }, + entry: input, extensions: vec![], }; let persisted: LogEntry = diff --git a/crates/worker/src/session_history.rs b/crates/worker/src/session_history.rs index 232a8c92d..472d40eb6 100644 --- a/crates/worker/src/session_history.rs +++ b/crates/worker/src/session_history.rs @@ -67,6 +67,16 @@ pub(crate) fn metadata( } } +pub(crate) fn compaction_use( + metadata: &SessionHistoryMetadata, +) -> session_store::InputCompactionUse { + metadata + .system_input + .as_ref() + .map(|input| input.usage().compaction) + .unwrap_or(session_store::InputCompactionUse::Retain) +} + #[cfg(test)] pub(crate) fn history_entry( item: Item, @@ -134,22 +144,10 @@ pub(crate) fn restore_history_entries( } *target = from_logged(entry); } - LogEntry::AnnotatedSystemItem { entry, .. } => { - history.push(HistoryEntry::new(entry.item.to_history_item(), { - let mut metadata = entry.metadata.clone(); - // Old typed startup records used generic placeholder origins. - // Preserve explicitly supplied metadata on current contributions. - if matches!( - metadata.origin, - WorkerHistoryProvenance::DerivedSummary - | WorkerHistoryProvenance::LegacyUnknown - ) && matches!(entry.item, session_store::SystemItem::JournalStartup { .. }) - { - metadata.origin = WorkerHistoryProvenance::JournalContext; - } - metadata - })) - } + LogEntry::AnnotatedSystemItem { entry, .. } => history.push(HistoryEntry::new( + entry.item.to_history_item(), + entry.metadata.clone(), + )), _ => {} } } diff --git a/crates/worker/src/skill.rs b/crates/worker/src/skill.rs index f9b15bf88..4629e03a8 100644 --- a/crates/worker/src/skill.rs +++ b/crates/worker/src/skill.rs @@ -8,6 +8,50 @@ pub use server_api::{ SkillProjectionStatus, SkillProvenance, SkillResourceRef, SkillSourceKind, }; +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct SkillActivationInput { + pub name: String, +} + +pub(crate) fn activation_input( + activation: &SkillActivationResponse, +) -> Result { + use session_store::*; + let body = format!( + "Agent Skill `{}` activated from {}.\n\n{}", + activation.name, activation.provenance.id, activation.body + ); + let payload = FeatureInputPayload::encode( + "skill:activation", + 1, + &SkillActivationInput { + name: activation.name.clone(), + }, + )?; + let metadata = SystemInputMetadata::new( + SystemInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::Retain, + }, + Some(payload), + Some(PublicSystemItem { + kind: "skill_activation".into(), + body: body.clone(), + presentation: PublicSystemPresentation::Context, + }), + )?; + Ok(LoggedSystemHistoryEntry::from_input( + LoggedSessionHistoryOrigin::Provided { + producer: "skill.activation".into(), + }, + None, + SystemInputEnvelope::new(body, metadata)?, + )) +} + #[derive(Debug, thiserror::Error)] pub enum SkillClientError { #[error("workspace client is unavailable: {0}")] diff --git a/crates/worker/src/subjektiv.rs b/crates/worker/src/subjektiv.rs index 40d4836c6..eb871f992 100644 --- a/crates/worker/src/subjektiv.rs +++ b/crates/worker/src/subjektiv.rs @@ -26,6 +26,30 @@ use server_api::*; use crate::{WorkspaceClient, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceServerOperation}; +impl + crate::Worker +{ + /// Bind live Host capability and its context provider, not authority inherited + /// from a previous binding. Ordinary Feature/lifecycle gates also apply to embedders. + pub fn bind_subjektiv_host(&mut self, host: Option>) { + self.set_subjektiv_host(host); + if self.manifest_lifecycle_features_enabled() + && self.backend_job().is_none() + && crate::feature::builtin::memory::ordinary_subjektiv_features_enabled(self.manifest()) + && let Ok(Some(connection)) = self.subjektiv_host_connection() + && (connection.host.journal_startup_granted() + || connection.host.journal_compaction_granted()) + { + self.install_session_context_source(Arc::new( + crate::feature::builtin::journal::JournalContextSource::new( + connection, + self.prompts(), + ), + )); + } + } +} + /// Host-issued execution context, never deserialized from model arguments. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SubjektivHostContext { @@ -1130,7 +1154,12 @@ enabled = true let initial = store.read_all(session, segment).unwrap(); let journal = initial.iter().find_map(|entry| match entry { LogEntry::AnnotatedSystemItem { entry, .. } - if matches!(entry.item, SystemItem::JournalStartup { .. }) => + if entry + .metadata + .system_input + .as_ref() + .and_then(|input| input.feature_payload()) + .is_some_and(|payload| payload.schema() == "journal:startup") => { Some(entry) } @@ -1140,22 +1169,33 @@ enabled = true let body = journal.map(|journal| { assert_eq!( journal.metadata.origin, - LoggedSessionHistoryOrigin::JournalContext + LoggedSessionHistoryOrigin::Provided { + producer: "subjektiv.journal".into() + } ); - let SystemItem::JournalStartup { - body, - snapshot: saved, - snapshot_sha256, - prompt_provenance, - } = &journal.item - else { + let SystemItem::Input { body } = &journal.item else { unreachable!() }; - assert_eq!(*saved, serde_json::to_value(&snapshot).unwrap()); + let payload = journal + .metadata + .system_input + .as_ref() + .unwrap() + .feature_payload() + .unwrap() + .decode::( + "journal:startup", + 1, + ) + .unwrap(); + let saved = serde_json::to_value(&payload.snapshot).unwrap(); + let snapshot_sha256 = &payload.snapshot_sha256; + let prompt_provenance = &payload.prompt_provenance; + assert_eq!(saved, serde_json::to_value(&snapshot).unwrap()); use sha2::{Digest, Sha256}; assert_eq!( *snapshot_sha256, - Sha256::digest(serde_json::to_vec(saved).unwrap()) + Sha256::digest(serde_json::to_vec(&saved).unwrap()) .iter() .map(|byte| format!("{byte:02x}")) .collect::() @@ -1168,7 +1208,7 @@ enabled = true body.trim_end(), format!( "## Prior Journal context\n\n```json\n{}\n```", - serde_json::to_string(saved).unwrap() + serde_json::to_string(&saved).unwrap() ) ); body.as_str() @@ -1237,7 +1277,7 @@ enabled = true } assert_eq!(host.startup_calls.load(Ordering::SeqCst), 1); let logs = store.read_all(session, segment).unwrap(); - assert_eq!(logs.iter().filter(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } if matches!(entry.item, SystemItem::JournalStartup { .. }))).count(), usize::from(contributes_context)); + assert_eq!(logs.iter().filter(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } if entry.metadata.system_input.as_ref().and_then(|input| input.feature_payload()).is_some_and(|payload| payload.schema() == "journal:startup"))).count(), usize::from(contributes_context)); controller .handle .send(Method::Shutdown { diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index fdde548d2..805ffb044 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -2757,11 +2757,25 @@ pub trait SystemItemCommitter: Send + Sync { .unwrap_or(WorkerHistoryProvenance::BackendInstruction { operation_id: None }), None, ); - let history = HistoryEntry::new(item.to_history_item(), metadata.clone()); + self.commit_annotated_system_item_then( + session_store::LoggedSystemHistoryEntry { item, metadata }, + extensions, + after_commit, + ) + } + + fn commit_annotated_system_item_then( + &self, + entry: session_store::LoggedSystemHistoryEntry, + extensions: Vec, + after_commit: &mut dyn FnMut(&HistoryEntry), + ) -> Result, StoreError> { + entry.validate_system_input()?; + let history = HistoryEntry::new(entry.item.to_history_item(), entry.metadata.clone()); self.commit_usage_boundary( LogEntry::AnnotatedSystemItem { ts: segment_log::now_millis(), - entry: session_store::LoggedSystemHistoryEntry { item, metadata }, + entry, extensions, }, &mut || after_commit(&history), @@ -2934,14 +2948,21 @@ pub(crate) enum SystemPromptContribution { pub(crate) struct ResidentContextRefresh { pub body: String, pub behavior_md: String, + pub prompt_provenance: Option, } -/// Initial-only System messages and Feature-owned metadata. Core persists and -/// replays each pair unchanged; it does not infer provenance from the payload. -/// Prepared with the head, never at restore/request hooks. +/// Feature-owned System context at explicit creation and compaction boundaries. +/// Core persists and replays each pair unchanged; normal restore never reloads it. #[async_trait::async_trait] -pub(crate) trait InitialContextSource: Send + Sync { +pub(crate) trait SessionContextSource: Send + Sync { async fn load(&self) -> Result, String>; + + async fn load_compaction_context( + &self, + _session: SessionId, + ) -> Result, String> { + Ok(Vec::new()) + } } #[async_trait::async_trait] @@ -3132,7 +3153,7 @@ pub struct Worker { inject_resident_summary: bool, /// Deferred resident prompt source installed by an enabled Feature. feature_resident_summary_source: Option>, - initial_context_source: Option>, + session_context_source: Option>, /// Exact complete seed retained across ambiguous Store responses. The committed /// segment remains authoritative; metadata must be repaired before accepting input. prepared_initial_seed: Option>, @@ -3404,7 +3425,7 @@ impl Worker { prompts, inject_resident_summary: true, feature_resident_summary_source: None, - initial_context_source: None, + session_context_source: None, prepared_initial_seed: None, subjektiv_host: None, restore_feature_lifecycle_pending: false, @@ -3452,8 +3473,8 @@ impl Worker { self.inject_resident_summary = enabled; } - pub(crate) fn install_initial_context_source(&mut self, source: Arc) { - self.initial_context_source = Some(source); + pub(crate) fn install_session_context_source(&mut self, source: Arc) { + self.session_context_source = Some(source); } pub(crate) fn install_system_prompt_contribution( @@ -3629,10 +3650,12 @@ impl Worker { self.workspace_context.client_handle() } - /// Bind a live capability before Feature installation. This is deliberately - /// separate from Workspace authority and is not inherited by SubWorkers. - pub fn bind_subjektiv_host(&mut self, host: Option>) { + pub(crate) fn set_subjektiv_host( + &mut self, + host: Option>, + ) { self.subjektiv_host = host; + self.session_context_source = None; } pub fn subjektiv_host_connection( @@ -3700,26 +3723,13 @@ impl Worker { { let activation = self.workspace_client().activate_skill(name)?; self.ensure_segment_head().await?; - let body = format!( - "Agent Skill `{}` activated from {}.\n\n{}", - activation.name, activation.provenance.id, activation.body - ); - let skill_metadata = new_history_metadata( - WorkerHistoryProvenance::BackendInstruction { operation_id: None }, - None, - ); + let input = crate::skill::activation_input(&activation).map_err(StoreError::from)?; + let history_entry = HistoryEntry::new(input.item.to_history_item(), input.metadata.clone()); self.commit_entry(LogEntry::AnnotatedSystemItem { ts: segment_log::now_millis(), - entry: session_store::LoggedSystemHistoryEntry { - item: SystemItem::SkillActivation { - name: activation.name.clone(), - body: body.clone(), - }, - metadata: skill_metadata.clone(), - }, + entry: input, extensions: Vec::new(), })?; - let history_entry = HistoryEntry::new(agen::Item::system_message(body), skill_metadata); let mut annotate = history_annotator( self.log_writer_handle(), vec![history_entry.clone()], @@ -4594,18 +4604,12 @@ impl Worker { .lock() .expect("restore system-item queue poisoned"), ); - for item in system_items { - let history_item = item.to_history_item(); - let metadata = new_history_metadata( - WorkerHistoryProvenance::BackendInstruction { operation_id: None }, - None, - ); + for entry in system_items { + let history_item = entry.item.to_history_item(); + let metadata = entry.metadata.clone(); self.commit_entry(LogEntry::AnnotatedSystemItem { ts: segment_log::now_millis(), - entry: session_store::LoggedSystemHistoryEntry { - item, - metadata: metadata.clone(), - }, + entry, extensions: Vec::new(), })?; let history_entry = HistoryEntry::new(history_item, metadata); @@ -5368,17 +5372,14 @@ impl Worker { .invocation_receipts .push(InvocationReceipt::new(invocation, Some(result.clone()))); } - let item = SystemItem::FeatureInvocationResult { result, body }; - let metadata = new_history_metadata( - WorkerHistoryProvenance::BackendInstruction { operation_id: None }, - None, - ); - let history = HistoryEntry::new(item.to_history_item(), metadata.clone()); + let input = + crate::feature::invocation::result_input(result, body).map_err(StoreError::from)?; + let history = HistoryEntry::new(input.item.to_history_item(), input.metadata.clone()); handle .writer .append_entry_locked(LogEntry::AnnotatedSystemItem { ts: segment_log::now_millis(), - entry: session_store::LoggedSystemHistoryEntry { item, metadata }, + entry: input, extensions: vec![pending_activation_extension(&completed)], })?; *state = completed; @@ -5882,7 +5883,7 @@ impl Worker { } else { if self.prepared_initial_seed.is_none() { self.ensure_system_prompt_materialized().await?; - let items = match &self.initial_context_source { + let items = match &self.session_context_source { Some(source) => source.load().await.map_err(WorkerError::InvalidState)?, None => Vec::new(), }; @@ -6939,11 +6940,18 @@ impl Worker { // Freeze one Host-owned read before starting the compactor. Background recording // remains independent; never re-read when building the replacement segment. - let journal_context = crate::feature::builtin::journal::freeze_compaction_journal( - self.subjektiv_host_connection(), - &self.prompts.load_full(), - ) - .map_err(WorkerError::PromptCatalog)?; + let frozen_context = match &self.session_context_source { + Some(source) => source + .load_compaction_context(self.session_id()) + .await + .map_err(WorkerError::InvalidState)?, + None => Vec::new(), + }; + for entry in &frozen_context { + entry + .validate_system_input() + .map_err(|e| WorkerError::InvalidState(e.to_string()))?; + } let history_entries = self.session.history().entries(); let retain_from = cut.index.min(history_entries.len()); let initial_summary = initial_compaction_summary(history_entries)?; @@ -6953,11 +6961,8 @@ impl Worker { let retained_history_entries = history_entries[retain_from..] .iter() .filter(|entry| { - !matches!( - entry.annotation.origin, - WorkerHistoryProvenance::CompactionSummary - | WorkerHistoryProvenance::JournalContext - ) + crate::session_history::compaction_use(&entry.annotation) + == session_store::InputCompactionUse::Retain }) .cloned() .collect::>(); @@ -6976,11 +6981,8 @@ impl Worker { let mut entries_to_summarise = history_entries[..retain_from] .iter() .filter(|entry| { - !matches!( - entry.annotation.origin, - WorkerHistoryProvenance::CompactionSummary - | WorkerHistoryProvenance::JournalContext - ) + crate::session_history::compaction_use(&entry.annotation) + == session_store::InputCompactionUse::Retain }) .cloned() .collect::>(); @@ -7067,24 +7069,29 @@ impl Worker { // Journals. In particular, a failed/empty read must not silently lose context // that the previous summary intentionally delegated to its injected Journal. // Carry them only into the compactor, never alongside the new reference seed. - for entry in history_entries - .iter() - .filter(|entry| entry.annotation.origin == WorkerHistoryProvenance::JournalContext) - { + for entry in history_entries.iter().filter(|entry| { + crate::session_history::compaction_use(&entry.annotation) + == session_store::InputCompactionUse::ReferenceInput + }) { if let Some(body) = entry.item.as_text() { summary_input.text.push_str( - "\n\n## Previous Journal reference (historical, not automatically retained)\n", + "\n\n## Previous reference input (historical, not automatically retained)\n", ); summary_input.text.push_str(body); summary_input .text - .push_str("\n## End previous Journal reference\n"); + .push_str("\n## End previous reference input\n"); } } // Mandatory context, outside overview truncation. The internal Worker's normal // request budget checks include this exact body and the full previous summary. summary_input.text.push_str("\n\n"); - summary_input.text.push_str(&journal_context); + for (index, entry) in frozen_context.iter().enumerate() { + if index != 0 { + summary_input.text.push_str("\n\n"); + } + summary_input.text.push_str(&entry.item.history_text()); + } if summary_input.warning_exceeded { self.alert( AlertLevel::Warn, @@ -7400,8 +7407,11 @@ impl Worker { crate::session_history::COMPACTION_SUMMARY_HEADER )); compact_introduced_system_messages.push(summary_message.clone()); - let journal_message = Item::system_message(journal_context); - compact_introduced_system_messages.push(journal_message.clone()); + let context_messages = frozen_context + .iter() + .map(|entry| entry.item.to_history_item()) + .collect::>(); + compact_introduced_system_messages.extend(context_messages.iter().cloned()); compact_introduced_system_messages.extend(auto_read_messages.iter().cloned()); if let Some(msg) = reference_message.as_ref() { compact_introduced_system_messages.push(msg.clone()); @@ -7414,7 +7424,7 @@ impl Worker { compact_introduced_system_messages.push(task_snapshot_message.clone()); new_history.push(summary_message); - new_history.push(journal_message); + new_history.extend(context_messages); new_history.extend(auto_read_messages); if let Some(msg) = reference_message { new_history.push(msg); @@ -7454,6 +7464,32 @@ impl Worker { .iter() .map(|entry| entry.annotation.entry_id.clone()) .collect::>(); + let generic_summary_input = session_store::SystemInputEnvelope::new( + new_history[0] + .as_text() + .expect("compaction summary is text"), + session_store::SystemInputMetadata::new( + session_store::SystemInputUsePolicy { + recording_material: session_store::InputEvidenceUse::Exclude, + memory_evidence: session_store::InputEvidenceUse::Exclude, + observation: session_store::InputObservationUse::Hidden, + compaction: session_store::InputCompactionUse::OpeningSummary, + }, + None, + None, + ) + .map_err(|e| WorkerError::InvalidState(e.to_string()))?, + ) + .map_err(|e| WorkerError::InvalidState(e.to_string()))?; + let summary_entry = session_store::LoggedSystemHistoryEntry::from_input( + WorkerHistoryProvenance::Provided { + producer: "worker.compaction".into(), + }, + Some(SessionHistoryDerivation { + sources: derived_sources.clone(), + }), + generic_summary_input, + ); let mut original_cursor = 0usize; let compacted_history_entries = new_history .iter() @@ -7461,25 +7497,9 @@ impl Worker { .enumerate() .map(|(index, item)| { if index == 0 { - HistoryEntry::new( - item, - new_history_metadata( - WorkerHistoryProvenance::CompactionSummary, - Some(SessionHistoryDerivation { - sources: derived_sources.clone(), - }), - ), - ) - } else if index == 1 { - HistoryEntry::new( - item, - new_history_metadata( - WorkerHistoryProvenance::JournalContext, - Some(SessionHistoryDerivation { - sources: derived_sources.clone(), - }), - ), - ) + HistoryEntry::new(item, summary_entry.metadata.clone()) + } else if let Some(context) = frozen_context.get(index - 1) { + HistoryEntry::new(item, context.metadata.clone()) } else if let Some((offset, original)) = original_entries[original_cursor..] .iter() .enumerate() @@ -7977,7 +7997,7 @@ where prompts: common.prompts, inject_resident_summary: true, feature_resident_summary_source: None, - initial_context_source: None, + session_context_source: None, prepared_initial_seed: None, subjektiv_host: None, restore_feature_lifecycle_pending: false, @@ -8358,7 +8378,7 @@ where prompts: common.prompts, inject_resident_summary: true, feature_resident_summary_source: None, - initial_context_source: None, + session_context_source: None, prepared_initial_seed: None, subjektiv_host: None, restore_feature_lifecycle_pending: false, @@ -8490,7 +8510,7 @@ where prompts: common.prompts, inject_resident_summary: true, feature_resident_summary_source: None, - initial_context_source: None, + session_context_source: None, prepared_initial_seed: None, subjektiv_host: None, restore_feature_lifecycle_pending: false, @@ -8901,7 +8921,7 @@ where prompts: common.prompts, inject_resident_summary: true, feature_resident_summary_source: None, - initial_context_source: None, + session_context_source: None, prepared_initial_seed: None, subjektiv_host: None, restore_feature_lifecycle_pending: true, @@ -9296,9 +9316,10 @@ fn summary_shrink_prompt( fn initial_compaction_summary( entries: &[HistoryEntry], ) -> Result, WorkerError> { - let mut summaries = entries - .iter() - .filter(|entry| entry.annotation.origin == WorkerHistoryProvenance::CompactionSummary); + let mut summaries = entries.iter().filter(|entry| { + crate::session_history::compaction_use(&entry.annotation) + == session_store::InputCompactionUse::OpeningSummary + }); let Some(entry) = summaries.next() else { return Ok(None); }; @@ -10739,13 +10760,32 @@ mod build_summary_prompt_tests { #[test] fn malformed_or_duplicate_host_summaries_fail_instead_of_omitting_context() { use crate::session_history::history_entry; - let valid = history_entry( - Item::system_message(format!( - "{}body", - crate::session_history::COMPACTION_SUMMARY_HEADER - )), - WorkerHistoryProvenance::CompactionSummary, - ); + let opening = |item| { + let mut entry = history_entry( + item, + WorkerHistoryProvenance::Provided { + producer: "arbitrary-summary-source".into(), + }, + ); + entry.annotation.system_input = Some( + session_store::SystemInputMetadata::new( + session_store::SystemInputUsePolicy { + recording_material: session_store::InputEvidenceUse::Exclude, + memory_evidence: session_store::InputEvidenceUse::Exclude, + observation: session_store::InputObservationUse::Hidden, + compaction: session_store::InputCompactionUse::OpeningSummary, + }, + None, + None, + ) + .unwrap(), + ); + entry + }; + let valid = opening(Item::system_message(format!( + "{}body", + crate::session_history::COMPACTION_SUMMARY_HEADER + ))); assert_eq!( initial_compaction_summary(std::slice::from_ref(&valid)) .unwrap() @@ -10757,13 +10797,7 @@ mod build_summary_prompt_tests { Item::user_message("pasted"), Item::system_message("missing marker"), ] { - assert!( - initial_compaction_summary(&[history_entry( - item, - WorkerHistoryProvenance::CompactionSummary - )]) - .is_err() - ); + assert!(initial_compaction_summary(&[opening(item)]).is_err()); } } @@ -11190,7 +11224,7 @@ mod build_summary_prompt_tests { 1 } LogEntry::AnnotatedSystemItem { entry, .. } - if matches!(entry.item, SystemItem::FeatureInvocationResult { .. }) => + if input_schema(entry) == Some("feature:invocation-result") => { 2 } @@ -14162,13 +14196,7 @@ permission = "write" let entries = store.read_all(session_id, segment_id).unwrap(); assert!(matches!( entries.last(), - Some(LogEntry::AnnotatedSystemItem { - entry: session_store::LoggedSystemHistoryEntry { - item: SystemItem::ResidentSummaryRefresh { .. }, - .. - }, - .. - }) + Some(LogEntry::AnnotatedSystemItem { entry, .. }) if input_schema(entry) == Some("memory:resident-summary") )); } } @@ -14465,15 +14493,10 @@ permission = "write" assert!(entries.iter().any(|entry| { matches!( entry, - LogEntry::AnnotatedSystemItem { - entry: session_store::LoggedSystemHistoryEntry { - item: SystemItem::SkillActivation { name, body }, - .. - }, - .. - } if name == "triage-errors" - && body.contains("# Triage Errors") - && body == history_text + LogEntry::AnnotatedSystemItem { entry, .. } + if entry.metadata.system_input.as_ref().and_then(|input| input.feature_payload()).and_then(|payload| payload.decode::("skill:activation", 1).ok()).is_some_and(|payload| payload.name == "triage-errors") + && entry.item.history_text().contains("# Triage Errors") + && entry.item.history_text() == history_text ) })); } @@ -15867,7 +15890,7 @@ permission = "write" .position(|entry| { matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } - if matches!(entry.item, SystemItem::FeatureInvocationResult { .. }) + if input_schema(entry) == Some("feature:invocation-result") ) }) .expect("invocation result must be durable before the model request"); @@ -15876,7 +15899,7 @@ permission = "write" .position(|entry| { matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } - if matches!(entry.item, SystemItem::SubjectBehaviorRefresh { .. }) + if input_schema(entry) == Some("subject:behavior") ) }) .expect("Subject behavior refresh must be durable before the model request"); @@ -16115,14 +16138,14 @@ permission = "write" .iter() .filter(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } - if matches!(entry.item, SystemItem::FeatureInvocationResult { .. }) + if input_schema(entry) == Some("feature:invocation-result") )) .count(), 2 // Explicit replay displays the cached result; it does not repeat the effect. ); assert!(entries.iter().any(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } - if matches!(entry.item, SystemItem::ResidentSummaryRefresh { .. }) + if input_schema(entry) == Some("memory:resident-summary") ))); } @@ -16165,7 +16188,7 @@ permission = "write" LogEntry::AnnotatedSystemItem { entry, extensions, .. } => { - matches!(entry.item, SystemItem::FeatureInvocationResult { .. }) + input_schema(entry) == Some("feature:invocation-result") && extensions.iter().any(|extension| { extension.domain == SESSION_PENDING_ACTIVATIONS_EXTENSION_DOMAIN && serde_json::from_value::( @@ -16756,34 +16779,70 @@ permission = "write" })); } - fn startup_test_item() -> SystemItem { - SystemItem::JournalStartup { - body: "PRIOR_JOURNAL_REFERENCE".into(), - snapshot: serde_json::json!({"subject_id":"subject", "journals":[], "omitted":false}), - snapshot_sha256: "snapshot-digest".into(), - prompt_provenance: PromptRenderProvenance { - workspace_id: None, - source_digest: "source".into(), - projection_digest: "projection".into(), - logical_name: "internal.subjektiv_journal_startup".into(), + fn input_schema(entry: &session_store::LoggedSystemHistoryEntry) -> Option<&str> { + entry + .metadata + .system_input + .as_ref()? + .feature_payload() + .map(|payload| payload.schema()) + } + + fn startup_test_input() -> session_store::LoggedSystemHistoryEntry { + use session_store::*; + let payload = FeatureInputPayload::encode( + "journal:startup", + 1, + &crate::feature::builtin::journal::JournalStartupInput { + snapshot: subjektiv::journal::startup::JournalStartupSnapshot { + subject_id: "subject".into(), + journals: vec![], + omitted: false, + }, + snapshot_sha256: "snapshot-digest".into(), + prompt_provenance: PromptRenderProvenance { + workspace_id: None, + source_digest: "source".into(), + projection_digest: "projection".into(), + logical_name: "internal.subjektiv_journal_startup".into(), + }, }, - } + ) + .unwrap(); + LoggedSystemHistoryEntry::from_input( + LoggedSessionHistoryOrigin::Provided { + producer: "arbitrary-context-provider".into(), + }, + None, + SystemInputEnvelope::new( + "PRIOR_JOURNAL_REFERENCE", + SystemInputMetadata::new( + SystemInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::ReferenceInput, + }, + Some(payload), + None, + ) + .unwrap(), + ) + .unwrap(), + ) } struct StartupTestSource { calls: Arc, fail: Arc, } #[async_trait] - impl InitialContextSource for StartupTestSource { + impl SessionContextSource for StartupTestSource { async fn load(&self) -> Result, String> { self.calls.fetch_add(1, Ordering::SeqCst); if self.fail.load(Ordering::SeqCst) { return Err("startup read failed".into()); } - Ok(vec![session_store::LoggedSystemHistoryEntry { - item: startup_test_item(), - metadata: new_history_metadata(WorkerHistoryProvenance::JournalContext, None), - }]) + Ok(vec![startup_test_input()]) } } @@ -16791,7 +16850,7 @@ permission = "write" async fn initial_system_messages_preserve_feature_metadata_without_payload_classification() { struct Source(Vec); #[async_trait] - impl InitialContextSource for Source { + impl SessionContextSource for Source { async fn load(&self) -> Result, String> { Ok(self.0.clone()) } @@ -16807,16 +16866,19 @@ permission = "write" }, metadata: new_history_metadata(WorkerHistoryProvenance::JournalContext, None), }, - session_store::LoggedSystemHistoryEntry { - item: startup_test_item(), - metadata: new_history_metadata( + { + let mut input = startup_test_input(); + let custom = new_history_metadata( WorkerHistoryProvenance::BackendInstruction { operation_id: Some("feature-selected-origin".into()), }, Some(SessionHistoryDerivation { sources: vec![SessionHistoryEntryId::new()], }), - ), + ); + input.metadata.origin = custom.origin; + input.metadata.derivation = custom.derivation; + input }, ]; let mut worker = Worker::new( @@ -16829,7 +16891,7 @@ permission = "write" ) .await .unwrap(); - worker.install_initial_context_source(Arc::new(Source(expected.clone()))); + worker.install_session_context_source(Arc::new(Source(expected.clone()))); worker.materialize_durable_session_head().await.unwrap(); let log = store .read_all(worker.session_id(), worker.segment_id()) @@ -16886,7 +16948,7 @@ permission = "write" .await .unwrap(); let calls = Arc::new(AtomicUsize::new(0)); - worker.install_initial_context_source(Arc::new(StartupTestSource { + worker.install_session_context_source(Arc::new(StartupTestSource { calls: calls.clone(), fail: Arc::new(AtomicBool::new(false)), })); @@ -16907,7 +16969,9 @@ permission = "write" assert_eq!(replay[0].item.as_text(), Some("PRIOR_JOURNAL_REFERENCE")); assert_eq!( replay[0].annotation.origin, - WorkerHistoryProvenance::JournalContext + WorkerHistoryProvenance::Provided { + producer: "arbitrary-context-provider".into() + } ); let view = crate::session_capture::SessionCapture::from_history_entries("startup", replay); @@ -16918,7 +16982,7 @@ permission = "write" let entries = store .read_all(worker.session_id(), worker.segment_id()) .unwrap(); - let journal = entries.iter().position(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } if matches!(entry.item, SystemItem::JournalStartup { .. }))).unwrap(); + let journal = entries.iter().position(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } if input_schema(entry) == Some("journal:startup"))).unwrap(); let user = entries .iter() .position(|entry| matches!(entry, LogEntry::AnnotatedUserInput { .. })) @@ -16944,7 +17008,7 @@ permission = "write" .unwrap(); let calls = Arc::new(AtomicUsize::new(0)); let fail = Arc::new(AtomicBool::new(true)); - worker.install_initial_context_source(Arc::new(StartupTestSource { + worker.install_session_context_source(Arc::new(StartupTestSource { calls: calls.clone(), fail: fail.clone(), })); @@ -16975,7 +17039,7 @@ permission = "write" .await .unwrap(); let calls = Arc::new(AtomicUsize::new(0)); - worker.install_initial_context_source(Arc::new(StartupTestSource { + worker.install_session_context_source(Arc::new(StartupTestSource { calls: calls.clone(), fail: Arc::new(AtomicBool::new(false)), })); @@ -17030,7 +17094,7 @@ permission = "write" .unwrap(); worker.enable_worker_metadata_write_through().unwrap(); let calls = Arc::new(AtomicUsize::new(0)); - worker.install_initial_context_source(Arc::new(StartupTestSource { + worker.install_session_context_source(Arc::new(StartupTestSource { calls: calls.clone(), fail: Arc::new(AtomicBool::new(false)), })); @@ -17052,7 +17116,7 @@ permission = "write" &manifest.worker.name, manifest.clone(), store.clone(), PromptCatalogSource::builtins_only(), WorkerWorkspaceContext::no_workspace(), WorkerFilesystemAuthority::None, Some(Box::new(NoopClient))).await.unwrap(); - restored.install_initial_context_source(Arc::new(StartupTestSource { + restored.install_session_context_source(Arc::new(StartupTestSource { calls: calls.clone(), fail: Arc::new(AtomicBool::new(true)), })); @@ -17377,7 +17441,7 @@ permission = "write" .iter() .filter_map(|entry| match entry { LogEntry::AnnotatedSystemItem { entry, .. } - if matches!(entry.item, SystemItem::JournalStartup { .. }) => + if input_schema(entry) == Some("journal:startup") => { Some(entry.item.clone()) } @@ -17428,7 +17492,7 @@ permission = "write" .iter() .filter_map(|entry| match entry { LogEntry::AnnotatedSystemItem { entry, .. } - if matches!(entry.item, SystemItem::JournalStartup { .. }) => + if input_schema(entry) == Some("journal:startup") => { Some(&entry.item) } diff --git a/crates/worker/tests/compact_events_test.rs b/crates/worker/tests/compact_events_test.rs index d72150e71..e0f773abf 100644 --- a/crates/worker/tests/compact_events_test.rs +++ b/crates/worker/tests/compact_events_test.rs @@ -2220,10 +2220,48 @@ fn summary_entry( origin: session_store::LoggedSessionHistoryOrigin, ) -> session_store::LoggedHistoryEntry { let mut entry = annotated(item); + entry.metadata.system_input = matches!( + &entry.item, + session_store::LoggedItem::Message { + role: session_store::LoggedRole::System, + .. + } + ) + .then(|| { + session_store::SystemInputMetadata::new( + session_store::SystemInputUsePolicy { + recording_material: session_store::InputEvidenceUse::Exclude, + memory_evidence: session_store::InputEvidenceUse::Exclude, + observation: session_store::InputObservationUse::Hidden, + compaction: match origin { + session_store::LoggedSessionHistoryOrigin::CompactionSummary => { + session_store::InputCompactionUse::OpeningSummary + } + session_store::LoggedSessionHistoryOrigin::JournalContext => { + session_store::InputCompactionUse::ReferenceInput + } + _ => session_store::InputCompactionUse::Retain, + }, + }, + None, + None, + ) + .unwrap() + }); entry.metadata.origin = origin; entry } +fn compaction_use( + metadata: &session_store::LoggedSessionHistoryMetadata, +) -> session_store::InputCompactionUse { + metadata + .system_input + .as_ref() + .map(|input| input.usage().compaction) + .unwrap_or(session_store::InputCompactionUse::Retain) +} + fn long_summary(label: &str) -> String { format!( "## Completed Tasks\n{label}: investigated the transport boundary.\n\n\ @@ -2359,7 +2397,8 @@ async fn full_initial_summary_survives_small_overview_budgets_and_both_retention assert_eq!( replacement .iter() - .filter(|entry| entry.metadata.origin == CompactionSummary) + .filter(|entry| compaction_use(&entry.metadata) + == session_store::InputCompactionUse::OpeningSummary) .count(), 1 ); @@ -2604,7 +2643,9 @@ async fn three_chained_compactions_transport_each_complete_predecessor_without_r let seed = replacement_seed(&fixture.worker); let inherited = seed .iter() - .filter(|entry| entry.metadata.origin == CompactionSummary) + .filter(|entry| { + compaction_use(&entry.metadata) == session_store::InputCompactionUse::OpeningSummary + }) .collect::>(); assert_eq!(inherited.len(), 1); assert_eq!( @@ -2892,7 +2933,7 @@ impl worker::subjektiv::SubjektivHost for CompactionJournalHost { } fn journal_startup_granted(&self) -> bool { - true + false } fn journal_startup_context( @@ -2996,8 +3037,8 @@ fn compaction_snapshot(generation: usize) -> subjektiv::journal::startup::Journa fn journal_body(worker: &Worker) -> String { let seed = replacement_seed(worker); assert_eq!( - seed[1].metadata.origin, - session_store::LoggedSessionHistoryOrigin::JournalContext + compaction_use(&seed[1].metadata), + session_store::InputCompactionUse::ReferenceInput ); let item = Item::from(seed[1].item.clone()); assert!(matches!( @@ -3009,8 +3050,8 @@ fn journal_body(worker: &Worker) -> String { )); assert_eq!( seed.iter() - .filter(|entry| entry.metadata.origin - == session_store::LoggedSessionHistoryOrigin::JournalContext) + .filter(|entry| compaction_use(&entry.metadata) + == session_store::InputCompactionUse::ReferenceInput) .count(), 1 ); @@ -3119,20 +3160,34 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai projection.source_digest = "a".repeat(64); let initial_catalog = Arc::new(worker::PromptCatalog::from_projection(projection).unwrap()); fixture.worker.prompts().store(initial_catalog.clone()); - let legacy_startup = session_store::LoggedSystemHistoryEntry { - item: session_store::SystemItem::JournalStartup { - body: "OLD_TYPED_STARTUP_JOURNAL".into(), - snapshot: serde_json::json!({"journals": []}), - snapshot_sha256: "legacy-startup-digest".into(), - prompt_provenance: session_store::PromptRenderProvenance { - workspace_id: None, - source_digest: "legacy-source".into(), - projection_digest: "legacy-projection".into(), - logical_name: "internal.subjektiv_journal_startup".into(), - }, - }, - metadata: summary_entry(Item::system_message(""), DerivedSummary).metadata, - }; + // Already migrated generic input with unrelated provenance/payload; Core + // must select it by explicit policy, not identify a Journal Feature. + let legacy_startup = session_store::LoggedSystemHistoryEntry::from_input( + DerivedSummary, + None, + session_store::SystemInputEnvelope::new( + "OLD_TYPED_STARTUP_JOURNAL", + session_store::SystemInputMetadata::new( + session_store::SystemInputUsePolicy { + recording_material: session_store::InputEvidenceUse::Exclude, + memory_evidence: session_store::InputEvidenceUse::Exclude, + observation: session_store::InputObservationUse::Hidden, + compaction: session_store::InputCompactionUse::ReferenceInput, + }, + Some( + session_store::FeatureInputPayload::encode( + "renamed-owner:reference", + 8, + &serde_json::json!({"private": "retained opaque context"}), + ) + .unwrap(), + ), + None, + ) + .unwrap(), + ) + .unwrap(), + ); let legacy_id = legacy_startup.metadata.entry_id.clone(); fixture .restore_seed_with_system( @@ -3166,7 +3221,7 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai .unwrap() .annotation .origin, - JournalContext + DerivedSummary ); let session = fixture.worker.session_id(); let original_segment = fixture.worker.segment_id(); @@ -3191,8 +3246,8 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai .unwrap() .annotation .origin, - JournalContext, - "legacy startup classification survives a normal turn" + DerivedSummary, + "generic source provenance survives a normal turn unchanged" ); } assert_eq!( @@ -3243,7 +3298,7 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai assert_eq!(host.reads.lock().unwrap().len(), index + 1); let history = fixture.worker.history(); let overview = input - .split("## Previous Journal reference (historical, not automatically retained)") + .split("## Previous reference input (historical, not automatically retained)") .next() .unwrap(); for old in &old_bodies { @@ -3336,6 +3391,17 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai .run_text("normal post-compaction turn") .await .unwrap(); + let frozen_seed = replacement_seed(&fixture.worker) + .into_iter() + .filter(|entry| { + matches!( + compaction_use(&entry.metadata), + session_store::InputCompactionUse::ReferenceInput + | session_store::InputCompactionUse::OpeningSummary + ) + }) + .collect::>(); + assert_eq!(frozen_seed.len(), 2); let records = fixture .worker .store() @@ -3375,6 +3441,20 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai .run_text("normal turn after typed history restore") .await .unwrap(); + for frozen in frozen_seed { + let replayed = fixture + .worker + .session_history() + .entries() + .iter() + .find(|entry| entry.annotation.entry_id == frozen.metadata.entry_id) + .expect("frozen entry identity survives re-restore"); + assert_eq!(replayed.item, Item::from(frozen.item)); + assert_eq!( + replayed.annotation, frozen.metadata, + "origin, derivation, public allowlist, policy and private payload survive atomic seed/restore" + ); + } assert_eq!( host.reads.lock().unwrap().len(), 3, @@ -3445,27 +3525,41 @@ async fn journal_absent_disabled_unavailable_and_over_byte_budget_keep_conversat ) .await; fixture.worker.compact(0).await.unwrap(); - let body = journal_body(&fixture.worker); - let expected = match case { - "no-host" | "disabled" => "disabled", - "absent" => "absent", - _ => "unavailable", - }; - assert!( - body.contains(&format!("Journal read status: {expected}.")), - "{case}" - ); let input = first_compactor_input(&observed.requests()[1]); - assert_frozen_journal_transport(&input, &body); - assert_journal_provenance( - &body, - expected_json.as_deref(), - &fixture.worker.prompts().load_full(), - (case != "no-host").then_some(COMPACTION_HOST_WORKSPACE_ID), - ); + let body = if matches!(case, "disabled" | "no-host") { + assert_eq!( + replacement_seed(&fixture.worker) + .iter() + .filter(|entry| compaction_use(&entry.metadata) + == session_store::InputCompactionUse::ReferenceInput) + .count(), + 0 + ); + assert!(!input.contains("Journal reference frozen at this compaction boundary")); + String::new() + } else { + let body = journal_body(&fixture.worker); + let expected = if case == "absent" { + "absent" + } else { + "unavailable" + }; + assert!( + body.contains(&format!("Journal read status: {expected}.")), + "{case}" + ); + assert_frozen_journal_transport(&input, &body); + assert_journal_provenance( + &body, + expected_json.as_deref(), + &fixture.worker.prompts().load_full(), + Some(COMPACTION_HOST_WORKSPACE_ID), + ); + body + }; assert_full_initial_summary(&input, &inherited); assert!(input.contains("UNRECORDED_FALLBACK_GOAL"), "{case}"); - assert!(input.contains("## Previous Journal reference (historical, not automatically retained)\nPREVIOUS_JOURNAL_DELEGATED_CONTEXT")); + assert!(input.contains("## Previous reference input (historical, not automatically retained)\nPREVIOUS_JOURNAL_DELEGATED_CONTEXT")); assert!( fixture.worker.history().iter().all(|item| !item .as_text() diff --git a/crates/worker/tests/feature_invocation_test.rs b/crates/worker/tests/feature_invocation_test.rs index a08cffda0..805f694ac 100644 --- a/crates/worker/tests/feature_invocation_test.rs +++ b/crates/worker/tests/feature_invocation_test.rs @@ -14,7 +14,7 @@ use protocol::{ InvocationArgumentDescriptor, InvocationArgumentType, InvocationArgumentValue, InvocationCompletion, InvocationValue, Segment, }; -use session_store::{CombinedStore, FsStore, FsWorkerStore, LogEntry, SystemItem}; +use session_store::{CombinedStore, FsStore, FsWorkerStore, LogEntry}; use worker::feature::{ FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureInvocationContext, FeatureInvocationHandler, FeatureInvocationHandlerError, FeatureModule, FeatureRegistryBuilder, @@ -224,10 +224,20 @@ fn results(worker: &Worker) -> Vec { .0 .into_iter() .filter_map(|entry| match entry { - LogEntry::AnnotatedSystemItem { entry, .. } => match entry.item { - SystemItem::FeatureInvocationResult { result, .. } => Some(result), - _ => None, - }, + LogEntry::AnnotatedSystemItem { entry, .. } => { + #[derive(serde::Deserialize)] + struct SavedResult { + result: FeatureInvocationResult, + } + entry + .metadata + .system_input + .as_ref()? + .feature_payload()? + .decode::("feature:invocation-result", 1) + .ok() + .map(|input| input.result) + } _ => None, }) .collect() diff --git a/crates/workspace-server/src/server/tests/subjektiv_journal_connected_tests.rs b/crates/workspace-server/src/server/tests/subjektiv_journal_connected_tests.rs index c3aca207b..893720ccd 100644 --- a/crates/workspace-server/src/server/tests/subjektiv_journal_connected_tests.rs +++ b/crates/workspace-server/src/server/tests/subjektiv_journal_connected_tests.rs @@ -547,30 +547,40 @@ async fn backend_real_runtime_journal_release_and_next_body_persisted_startup() let segments = session.list_segments(session_id).unwrap(); assert_eq!(segments.len(), 1); let entries = session.read_all_read_only(session_id, segments[0]).unwrap(); + #[derive(serde::Deserialize)] + struct SavedJournal { + snapshot: serde_json::Value, + snapshot_sha256: String, + prompt_provenance: session_store::PromptRenderProvenance, + } let startup: Vec<_> = entries .iter() .enumerate() - .filter_map(|(index, log)| match log { - session_store::LogEntry::AnnotatedSystemItem { entry, .. } => match &entry.item { - session_store::SystemItem::JournalStartup { - snapshot, - snapshot_sha256, - body, - prompt_provenance, - } => Some((index, snapshot, snapshot_sha256, body, prompt_provenance)), - _ => None, - }, - _ => None, + .filter_map(|(index, log)| { + let session_store::LogEntry::AnnotatedSystemItem { entry, .. } = log else { + return None; + }; + let payload = entry + .metadata + .system_input + .as_ref()? + .feature_payload()? + .decode::("journal:startup", 1) + .ok()?; + Some((index, payload, entry.item.history_text())) }) .collect(); assert_eq!(startup.len(), 1, "startup must be durably committed once"); - let (startup_index, snapshot, digest, body, provenance) = startup[0]; + let (startup_index, payload, body) = &startup[0]; + let snapshot = &payload.snapshot; + let digest = &payload.snapshot_sha256; + let provenance = &payload.prompt_provenance; let first_input = entries .iter() .position(|entry| matches!(entry, session_store::LogEntry::AnnotatedUserInput { .. })) .expect("new body persisted its initial human input"); assert!( - startup_index < first_input, + *startup_index < first_input, "startup reference must be persisted before the first human input/model request" ); assert_eq!(snapshot["subject_id"], subject.id); diff --git a/tools/state-contract-name-exceptions.json b/tools/state-contract-name-exceptions.json index 07fa26137..933d7a696 100644 --- a/tools/state-contract-name-exceptions.json +++ b/tools/state-contract-name-exceptions.json @@ -70,12 +70,12 @@ ] }, { - "path": "crates/session-store/src/system_item.rs", + "path": "crates/session-store/src/legacy_session_log/tests/v5.rs", "kind": "saved_data", - "reason": "frozen_subject_behavior_log_reads_old_counter_without_republishing_itの保存済みSystemItem fixture。旧JSONを読んでもexact archived bodyを維持することを証明する。削除すると過去Session log形式の読取回帰を検出できない。", + "reason": "旧Subject behavior JSONに保存されていたcounterの実fixture。v1–4専用migration境界で旧綴りを読み、本文を維持して現行payloadへcounterを再公開しないことを検証する。通常v5 decoderは旧Feature kind自体を拒否する。", "lines": [ { - "sha256": "5b5998029cfed30634e8b063b759aefd70e30dab3d02ce9defe9086977fc9811", + "sha256": "54c30cb3198eb16e57c7e580fce4dc058f89e793b77a2955be317aa38b490138", "count": 1 } ] From 86602240fad18a3b2028fc5602ed83a020a79060 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 03:52:55 +0900 Subject: [PATCH 07/11] refactor: bind Subject metadata and prompt confirmation to Host lifecycle --- crates/session-store/src/worker_metadata.rs | 64 ++-- crates/worker/src/feature/builtin.rs | 10 + crates/worker/src/feature/builtin/memory.rs | 63 ++-- crates/worker/src/lib.rs | 2 + crates/worker/src/metadata_lifecycle.rs | 69 ++++ crates/worker/src/subjektiv_metadata.rs | 120 +++++++ crates/worker/src/worker.rs | 377 ++++++++++++++++---- 7 files changed, 569 insertions(+), 136 deletions(-) create mode 100644 crates/worker/src/metadata_lifecycle.rs create mode 100644 crates/worker/src/subjektiv_metadata.rs diff --git a/crates/session-store/src/worker_metadata.rs b/crates/session-store/src/worker_metadata.rs index e2a8ffe72..997419ba7 100644 --- a/crates/session-store/src/worker_metadata.rs +++ b/crates/session-store/src/worker_metadata.rs @@ -210,18 +210,6 @@ impl WorkerMetadata { } } -fn replace_active_session(metadata: &mut WorkerMetadata, active: Option) { - if let Some(session_id) = active.as_ref().map(|active| active.session_id) - && metadata - .subjektiv_session_attribution - .as_ref() - .is_some_and(|state| state.session_id() != session_id) - { - metadata.subjektiv_session_attribution = None; - } - metadata.active = active; -} - /// Sync persistence backend for Worker metadata. pub trait WorkerMetadataStore: Send + Sync { /// Create or replace metadata for its `worker_name` key. @@ -297,7 +285,7 @@ pub trait WorkerMetadataStore: Send + Sync { workspace_root: Option, ) -> Result { self.update_by_name(worker_name, |metadata| { - replace_active_session(metadata, active); + metadata.active = active; metadata.resolved_manifest_snapshot = resolved_manifest_snapshot; if let Some(workspace_id) = workspace_id { metadata.workspace_id = Some(workspace_id); @@ -317,7 +305,7 @@ pub trait WorkerMetadataStore: Send + Sync { workspace_id: Option, ) -> Result { self.update_by_name(worker_name, |metadata| { - replace_active_session(metadata, active); + metadata.active = active; metadata.resolved_manifest_snapshot = resolved_manifest_snapshot; if let Some(workspace_id) = workspace_id { metadata.workspace_id = Some(workspace_id); @@ -334,7 +322,7 @@ pub trait WorkerMetadataStore: Send + Sync { workspace_root: Option, ) -> Result { self.update_by_name(worker_name, |metadata| { - replace_active_session(metadata, active); + metadata.active = active; metadata.resolved_manifest_snapshot = resolved_manifest_snapshot; if let Some(workspace_root) = workspace_root { metadata.workspace_root = Some(workspace_root); @@ -342,18 +330,6 @@ pub trait WorkerMetadataStore: Send + Sync { }) } - /// Set the Host-owned Subjektiv Session attribution lifecycle state while - /// preserving the active Session pointer and every unrelated field. - fn set_subjektiv_session_attribution( - &self, - worker_name: &str, - state: SubjektivSessionAttributionState, - ) -> Result { - self.update_by_name(worker_name, |metadata| { - metadata.subjektiv_session_attribution = Some(state); - }) - } - /// Set spawned-child registry state while preserving active pointer and manifest snapshot. fn set_spawned_children( &self, @@ -1087,13 +1063,13 @@ mod tests { )) .unwrap(); store - .set_subjektiv_session_attribution( - "subject-worker", - SubjektivSessionAttributionState::OutcomeUnknown { - session_id, - diagnostic: "request interrupted".to_string(), - }, - ) + .update_by_name("subject-worker", |metadata| { + metadata.subjektiv_session_attribution = + Some(SubjektivSessionAttributionState::OutcomeUnknown { + session_id, + diagnostic: "request interrupted".to_string(), + }); + }) .unwrap(); store .set_active( @@ -1117,7 +1093,7 @@ mod tests { } #[test] - fn new_active_session_clears_prior_session_attribution_state() { + fn generic_active_update_does_not_interpret_host_attribution() { let tmp = tempfile::TempDir::new().unwrap(); let store = FsWorkerStore::new(tmp.path()).unwrap(); let old_session_id = crate::new_session_id(); @@ -1129,13 +1105,13 @@ mod tests { )) .unwrap(); store - .set_subjektiv_session_attribution( - "subject-worker", - SubjektivSessionAttributionState::Confirmed { - session_id: old_session_id, - subject_id: "subject-1".to_string(), - }, - ) + .update_by_name("subject-worker", |metadata| { + metadata.subjektiv_session_attribution = + Some(SubjektivSessionAttributionState::Confirmed { + session_id: old_session_id, + subject_id: "subject-1".to_string(), + }); + }) .unwrap(); store @@ -1151,7 +1127,9 @@ mod tests { restored.active, Some(WorkerActiveSegmentRef::pending_segment(new_session_id)) ); - assert!(restored.subjektiv_session_attribution.is_none()); + assert!( + matches!(restored.subjektiv_session_attribution, Some(SubjektivSessionAttributionState::Confirmed { session_id, .. }) if session_id == old_session_id) + ); } #[test] diff --git a/crates/worker/src/feature/builtin.rs b/crates/worker/src/feature/builtin.rs index a68b8f992..63e944e7b 100644 --- a/crates/worker/src/feature/builtin.rs +++ b/crates/worker/src/feature/builtin.rs @@ -5,6 +5,16 @@ //! an external plugin-loading surface. pub mod backend_job_result; + +/// Host metadata participants are independent of model-facing Tool enablement. +/// Disabling a Feature must not preserve stale Session-bound authority evidence. +pub(crate) fn session_metadata_lifecycles() +-> Vec> { + vec![std::sync::Arc::new( + crate::subjektiv_metadata::SessionAttributionMetadata, + )] +} + pub mod chat_invocation; pub mod drive; pub mod flow_transition; diff --git a/crates/worker/src/feature/builtin/memory.rs b/crates/worker/src/feature/builtin/memory.rs index f6f5869e6..ed90b8622 100644 --- a/crates/worker/src/feature/builtin/memory.rs +++ b/crates/worker/src/feature/builtin/memory.rs @@ -725,7 +725,7 @@ struct SubjectBehaviorRefreshState { struct WorkspaceSubjektivResidentSummarySource { host: SubjektivHostConnection, prompts: Arc>, - behavior_state: Mutex, + behavior_state: Arc>, } fn resident_summary_contribution( @@ -816,44 +816,49 @@ impl WorkspaceSubjektivResidentSummarySource { }, )) } - - fn remember_behavior(&self, output: &server_api::SubjektivResidentContextOutput) { - let mut state = self - .behavior_state - .lock() - .expect("subject behavior state poisoned"); - state.applied = Some(output.behavior_md.clone()); - } } /// Detect behavior changes against the state reflected in history, not the fetched state. /// -/// Initial load builds a prompt contribution; change refresh does not advance applied on fetch/render alone. -/// The interceptor confirms after durably appending a system item, allowing refetch if append fails. -/// Invalidate applied before rewrite so even identical bodies can be presented in the new history. +/// Fetch/render never advances applied state. Initial preparation captures a +/// source-local completion, invoked only after its prompt and active pointer commit. +/// Later refreshes are acknowledged by the interceptor after durable append. +/// Invalidate applied before rewrite so identical bodies can be represented again. #[async_trait] impl SystemPromptContributionSource for WorkspaceSubjektivResidentSummarySource { async fn load(&self) -> SystemPromptContribution { + self.load_initial().await.0 + } + + async fn load_initial( + &self, + ) -> ( + SystemPromptContribution, + Option, + ) { match self.fetch().await { Ok(output) => match self.render(&output) { Ok((rendered, _)) => { - self.remember_behavior(&output); - SystemPromptContribution::Ready(rendered) + let state = self.behavior_state.clone(); + let behavior = output.behavior_md; + ( + SystemPromptContribution::Ready(rendered), + Some(Box::new(move || { + state + .lock() + .expect("subject behavior state poisoned") + .applied = Some(behavior); + })), + ) } Err(error) => { tracing::error!(%error, "subject resident context render failed"); - SystemPromptContribution::Ready( - "The Host could not render the Subject resident context. Do not treat this as unset behavior or infer replacement behavior from Memory." - .to_string(), - ) + (SystemPromptContribution::Ready("The Host could not render the Subject resident context. Do not treat this as unset behavior or infer replacement behavior from Memory.".into()), None) } }, Err(error) => { tracing::error!(%error, "subject resident context fetch failed"); - SystemPromptContribution::Ready( - "The Host could not fetch the Subject resident context. Do not treat this as unset behavior or infer replacement behavior from Memory." - .to_string(), - ) + (SystemPromptContribution::Ready("The Host could not fetch the Subject resident context. Do not treat this as unset behavior or infer replacement behavior from Memory.".into()), None) } } } @@ -1218,7 +1223,7 @@ pub(crate) fn ordinary_subjektiv_resident_summary_source( Ok(Some(Arc::new(WorkspaceSubjektivResidentSummarySource { host: host.expect("checked explicit connection"), prompts, - behavior_state: Mutex::new(SubjectBehaviorRefreshState::default()), + behavior_state: Arc::new(Mutex::new(SubjectBehaviorRefreshState::default())), }))) } @@ -1835,8 +1840,16 @@ permission = "write" .unwrap() .unwrap(); - let initial = source.load().await; + let (initial, completion) = source.load_initial().await; assert!(matches!(initial, SystemPromptContribution::Ready(_))); + assert!( + source + .load_changed_resident_context() + .await + .unwrap() + .is_some() + ); + completion.expect("successful initial contribution must provide completion")(); assert!( source .load_changed_resident_context() @@ -1907,7 +1920,7 @@ permission = "write" // target-side bounded resident request contract. assert_eq!( client.timeouts.lock().unwrap().as_slice(), - &[RESIDENT_SUMMARY_REQUEST_TIMEOUT; 7] + &[RESIDENT_SUMMARY_REQUEST_TIMEOUT; 8] ); } diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 10cf5a6c5..13d61103b 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -15,6 +15,7 @@ pub mod hook; pub(crate) mod in_flight; pub mod ipc; pub mod job; +mod metadata_lifecycle; pub mod model_client; mod paste_artifact_tool; pub mod prompt; @@ -26,6 +27,7 @@ mod session_history; pub mod shared_state; mod shutdown_after_idle; mod subject_release; +mod subjektiv_metadata; pub use subject_release::SubjectReleaseReservation; pub mod skill; pub mod spawn; diff --git a/crates/worker/src/metadata_lifecycle.rs b/crates/worker/src/metadata_lifecycle.rs new file mode 100644 index 000000000..074e86d60 --- /dev/null +++ b/crates/worker/src/metadata_lifecycle.rs @@ -0,0 +1,69 @@ +//! Atomic metadata transitions with Host-owned Session lifecycle participants. +//! +//! Participants inspect domain evidence inside the metadata update, not after the +//! active pointer has been published. The generic writer never interprets it. + +use session_store::worker_metadata::{ + WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore, WorkerStoreError, +}; +use std::sync::Arc; + +pub(crate) trait SessionMetadataLifecycle: Send + Sync { + /// Runs under the metadata update lock. Only compare/transform the supplied + /// value: no I/O, reentrant store calls, lock waits, or Feature refresh. + fn active_changing(&self, metadata: &mut WorkerMetadata, next: Option<&WorkerActiveSegmentRef>); +} + +pub(crate) type WorkerMetadataWriter = + Arc Result<(), WorkerStoreError> + Send + Sync>; +pub(crate) type WorkerMetadataSegmentCas = Arc< + dyn Fn(&str, &WorkerActiveSegmentRef, WorkerActiveSegmentRef) -> Result + + Send + + Sync, +>; + +pub(crate) fn worker_metadata_writer_for_store(store: &St) -> WorkerMetadataWriter +where + St: WorkerMetadataStore + Clone + Send + Sync + 'static, +{ + let store = store.clone(); + let participants = crate::feature::builtin::session_metadata_lifecycles(); + Arc::new(move |next| { + store + .update_by_name(&next.worker_name, |metadata| { + for participant in &participants { + participant.active_changing(metadata, next.active.as_ref()); + } + metadata.active = next.active; + metadata.resolved_manifest_snapshot = next.resolved_manifest_snapshot; + if next.workspace_id.is_some() { + metadata.workspace_id = next.workspace_id; + } + if next.workspace_root.is_some() { + metadata.workspace_root = next.workspace_root; + } + }) + .map(|_| ()) + }) +} + +pub(crate) fn worker_metadata_segment_cas_for_store(store: &St) -> WorkerMetadataSegmentCas +where + St: WorkerMetadataStore + Clone + Send + Sync + 'static, +{ + let store = store.clone(); + let participants = crate::feature::builtin::session_metadata_lifecycles(); + Arc::new(move |worker_name, expected, replacement| { + let mut matched = false; + store.update_by_name(worker_name, |metadata| { + if metadata.active.as_ref() == Some(expected) { + for participant in &participants { + participant.active_changing(metadata, Some(&replacement)); + } + metadata.active = Some(replacement); + matched = true; + } + })?; + Ok(matched) + }) +} diff --git a/crates/worker/src/subjektiv_metadata.rs b/crates/worker/src/subjektiv_metadata.rs new file mode 100644 index 000000000..e24b4c926 --- /dev/null +++ b/crates/worker/src/subjektiv_metadata.rs @@ -0,0 +1,120 @@ +//! Host-owned attribution transitions. Metadata callbacks are synchronous, local +//! comparisons only: no network I/O, Feature lookup, or nested store access. + +use crate::metadata_lifecycle::SessionMetadataLifecycle; +use session_store::{ + SessionId, SubjektivSessionAttributionState, WorkerActiveSegmentRef, WorkerMetadata, + WorkerMetadataStore, +}; + +pub(crate) struct SessionAttributionMetadata; + +impl SessionMetadataLifecycle for SessionAttributionMetadata { + fn active_changing( + &self, + metadata: &mut WorkerMetadata, + next: Option<&WorkerActiveSegmentRef>, + ) { + if metadata.active.as_ref().map(|active| active.session_id) + != next.map(|active| active.session_id) + { + metadata.subjektiv_session_attribution = None; + } + } +} + +pub(crate) fn compare_and_swap_attribution( + store: &St, + worker_name: &str, + session_id: SessionId, + expected: Option<&SubjektivSessionAttributionState>, + replacement: SubjektivSessionAttributionState, +) -> Result<(), crate::WorkerError> { + let mut matched = false; + store.update_by_name(worker_name, |metadata| { + if metadata.active.as_ref().map(|active| active.session_id) == Some(session_id) + && metadata.subjektiv_session_attribution.as_ref() == expected + && replacement.session_id() == session_id + { + metadata.subjektiv_session_attribution = Some(replacement); + matched = true; + } + })?; + if !matched { + return Err(crate::WorkerError::SubjektivSessionAttribution { + state: "attribution_conflict", + message: "active Session or observed attribution changed during Host confirmation" + .into(), + }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + //! Connect the production metadata writer/CAS and Host lifecycle to a real + //! filesystem store; no metadata or persistence boundary is substituted. + use super::*; + use crate::metadata_lifecycle::{ + worker_metadata_segment_cas_for_store, worker_metadata_writer_for_store, + }; + + #[test] + fn host_transition_preserves_same_session_and_invalidates_new_session_atomically() { + let dir = tempfile::tempdir().unwrap(); + let store = session_store::FsWorkerStore::new(dir.path()).unwrap(); + let session = session_store::new_session_id(); + let mut metadata = WorkerMetadata::new( + "body", + Some(WorkerActiveSegmentRef::pending_segment(session)), + ); + metadata.workspace_id = Some("workspace".into()); + metadata.subjektiv_session_attribution = + Some(SubjektivSessionAttributionState::Confirmed { + session_id: session, + subject_id: "subject".into(), + }); + store.write(&metadata).unwrap(); + let writer = worker_metadata_writer_for_store(&store); + let cas = worker_metadata_segment_cas_for_store(&store); + let segment = + WorkerActiveSegmentRef::active_segment(session, session_store::new_segment_id()); + let mut next = WorkerMetadata::new("body", Some(segment.clone())); + writer(next.clone()).unwrap(); + let materialized = store.read_by_name("body").unwrap().unwrap(); + assert_eq!( + materialized.subjektiv_session_attribution, + metadata.subjektiv_session_attribution + ); + assert_eq!(materialized.workspace_id, metadata.workspace_id); + let compacted = + WorkerActiveSegmentRef::active_segment(session, session_store::new_segment_id()); + assert!(cas("body", &segment, compacted.clone()).unwrap()); + assert_eq!( + store + .read_by_name("body") + .unwrap() + .unwrap() + .subjektiv_session_attribution, + metadata.subjektiv_session_attribution + ); + let new_session = WorkerActiveSegmentRef::pending_segment(session_store::new_session_id()); + assert!(!cas("body", &segment, new_session.clone()).unwrap()); + assert!(cas("body", &compacted, new_session.clone()).unwrap()); + let switched = store.read_by_name("body").unwrap().unwrap(); + assert_eq!(switched.active, Some(new_session)); + assert!(switched.subjektiv_session_attribution.is_none()); + assert_eq!(switched.workspace_id, metadata.workspace_id); + // Returning to the old Session cannot recover the invalidated evidence. + next.active = metadata.active; + writer(next).unwrap(); + assert!( + store + .read_by_name("body") + .unwrap() + .unwrap() + .subjektiv_session_attribution + .is_none() + ); + } +} diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 805ffb044..aabb6314f 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -1676,41 +1676,10 @@ pub struct SegmentLocation { pub segment_id: SegmentId, } -type WorkerMetadataWriter = - Arc Result<(), WorkerStoreError> + Send + Sync>; -type WorkerMetadataSegmentCas = Arc< - dyn Fn(&str, &WorkerActiveSegmentRef, WorkerActiveSegmentRef) -> Result - + Send - + Sync, ->; - -fn worker_metadata_writer_for_store(store: &St) -> WorkerMetadataWriter -where - St: WorkerMetadataStore + Clone + Send + Sync + 'static, -{ - let store = store.clone(); - Arc::new(move |metadata| { - store - .set_active_with_workspace_context( - &metadata.worker_name, - metadata.active, - metadata.resolved_manifest_snapshot, - metadata.workspace_id, - metadata.workspace_root, - ) - .map(|_| ()) - }) -} - -fn worker_metadata_segment_cas_for_store(store: &St) -> WorkerMetadataSegmentCas -where - St: WorkerMetadataStore + Clone + Send + Sync + 'static, -{ - let store = store.clone(); - Arc::new(move |worker_name, expected, replacement| { - store.compare_and_swap_active(worker_name, expected, replacement) - }) -} +use crate::metadata_lifecycle::{ + WorkerMetadataSegmentCas, WorkerMetadataWriter, worker_metadata_segment_cas_for_store, + worker_metadata_writer_for_store, +}; /// Lock-free shared session/segment pointer. /// @@ -2938,6 +2907,8 @@ impl WorkerSession { } } +pub(crate) type PromptContributionCompletion = Box; + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum SystemPromptContribution { Ready(String), @@ -2969,8 +2940,21 @@ pub(crate) trait SessionContextSource: Send + Sync { pub(crate) trait SystemPromptContributionSource: Send + Sync { async fn load(&self) -> SystemPromptContribution; - /// Returns a durable model-visible refresh only when user-managed resident - /// context changed since the last successful load. + /// Prepare an initial contribution with an optional source-bound completion. + /// Completion runs only after its exact prompt and active pointer commit, + /// under the publication/release fence. It must perform bounded local updates + /// only; no I/O, store reentry, or authority grants. Restore never replays it. + async fn load_initial( + &self, + ) -> ( + SystemPromptContribution, + Option, + ) { + (self.load().await, None) + } + + /// Returns a model-visible refresh only when user-managed resident context + /// changed since its last committed representation. async fn load_changed_resident_context( &self, ) -> Result, String> { @@ -3157,6 +3141,7 @@ pub struct Worker { /// Exact complete seed retained across ambiguous Store responses. The committed /// segment remains authoritative; metadata must be repaired before accepting input. prepared_initial_seed: Option>, + prepared_initial_prompt_completion: Option<(String, PromptContributionCompletion)>, subjektiv_host: Option>, /// One-shot generic Feature lifecycle notification required after restore. restore_feature_lifecycle_pending: bool, @@ -3427,6 +3412,7 @@ impl Worker { feature_resident_summary_source: None, session_context_source: None, prepared_initial_seed: None, + prepared_initial_prompt_completion: None, subjektiv_host: None, restore_feature_lifecycle_pending: false, feature_system_prompt_override: None, @@ -3482,6 +3468,7 @@ impl Worker { resident_summary_source: Option>, system_prompt_override: Option, ) { + self.prepared_initial_prompt_completion = None; self.feature_resident_summary_source = resident_summary_source; self.feature_system_prompt_override = system_prompt_override; } @@ -4671,16 +4658,19 @@ impl Worker { } } } + let mut completion = None; let resident_summary = if self.inject_resident_summary { match &self.feature_resident_summary_source { - Some(source) => match source.load().await { - SystemPromptContribution::Ready(summary) if !summary.is_empty() => { - Some(summary) + Some(source) => { + let (value, prepared) = source.load_initial().await; + match value { + SystemPromptContribution::Ready(summary) if !summary.is_empty() => { + completion = prepared; + Some(summary) + } + _ => None, } - SystemPromptContribution::Ready(_) | SystemPromptContribution::Unavailable => { - None - } - }, + } None => None, } } else { @@ -4707,6 +4697,12 @@ impl Worker { let rendered = template .render(&ctx) .map_err(|source| WorkerError::SystemPromptRender { source })?; + self.prepared_initial_prompt_completion = completion.and_then(|complete| { + resident_summary + .as_ref() + .filter(|body| rendered.contains(body.as_str())) + .map(|_| (rendered.clone(), complete)) + }); self.engine .as_mut() .expect("worker present") @@ -5959,6 +5955,11 @@ impl Worker { self.system_prompt_template = None; self.feature_system_prompt_override = None; self.prepared_initial_seed = None; + if let Some((prepared, complete)) = self.prepared_initial_prompt_completion.take() { + if system_prompt.as_ref() == Some(&prepared) { + complete(); + } + } Ok(()) } @@ -7999,6 +8000,7 @@ where feature_resident_summary_source: None, session_context_source: None, prepared_initial_seed: None, + prepared_initial_prompt_completion: None, subjektiv_host: None, restore_feature_lifecycle_pending: false, feature_system_prompt_override: None, @@ -8029,6 +8031,7 @@ where subjektiv_attached: bool, lifecycle: SubjektivSessionAttributionLifecycle, ) -> Result<(), WorkerError> { + let _admission = self.segment_state.mutation_authority.enter()?; let worker_name = self.manifest.worker.name.as_str(); let metadata = self.store.read_by_name(worker_name)?.ok_or_else(|| { WorkerError::SubjektivSessionAttribution { @@ -8107,11 +8110,15 @@ where } else { "legacy Session has no local attribution evidence; immutable Backend verification started" }; - self.persist_subjektiv_session_attribution(incomplete_subjektiv_attribution_state( + let pending_attribution = incomplete_subjektiv_attribution_state( session_id, create_if_missing, initial_diagnostic.to_string(), - ))?; + ); + self.persist_subjektiv_session_attribution( + metadata.subjektiv_session_attribution.as_ref(), + pending_attribution.clone(), + )?; let output = if self.subjektiv_host.is_some() { let connection = self .subjektiv_host_connection() @@ -8144,7 +8151,7 @@ where message.clone(), ) }; - self.persist_subjektiv_session_attribution(state)?; + self.persist_subjektiv_session_attribution(Some(&pending_attribution), state)?; return Err(WorkerError::SubjektivSessionAttribution { state: if create_if_missing && rejected { "failed" @@ -8170,6 +8177,7 @@ where "Session attribution transport outcome is unknown: {error}" )); self.persist_subjektiv_session_attribution( + Some(&pending_attribution), incomplete_subjektiv_attribution_state( session_id, create_if_missing, @@ -8198,7 +8206,7 @@ where diagnostic: message.clone(), } }; - self.persist_subjektiv_session_attribution(state)?; + self.persist_subjektiv_session_attribution(Some(&pending_attribution), state)?; return Err(WorkerError::SubjektivSessionAttribution { state: if create_if_missing { "failed" @@ -8216,6 +8224,7 @@ where "Session attribution response outcome is unknown: {error}" )); self.persist_subjektiv_session_attribution( + Some(&pending_attribution), incomplete_subjektiv_attribution_state( session_id, create_if_missing, @@ -8237,20 +8246,26 @@ where let message = "Session attribution response did not confirm the requested immutable identity" .to_string(); - self.persist_subjektiv_session_attribution(incomplete_subjektiv_attribution_state( - session_id, - create_if_missing, - message.clone(), - ))?; + self.persist_subjektiv_session_attribution( + Some(&pending_attribution), + incomplete_subjektiv_attribution_state( + session_id, + create_if_missing, + message.clone(), + ), + )?; return Err(WorkerError::SubjektivSessionAttribution { state: incomplete_subjektiv_attribution_label(create_if_missing), message, }); } - self.persist_subjektiv_session_attribution(SubjektivSessionAttributionState::Confirmed { - session_id, - subject_id: output.subject_id.clone(), - })?; + self.persist_subjektiv_session_attribution( + Some(&pending_attribution), + SubjektivSessionAttributionState::Confirmed { + session_id, + subject_id: output.subject_id.clone(), + }, + )?; self.retain_journal_source_before_publication(session_id, &output.subject_id) } @@ -8291,11 +8306,16 @@ where fn persist_subjektiv_session_attribution( &self, + expected: Option<&SubjektivSessionAttributionState>, state: SubjektivSessionAttributionState, ) -> Result<(), WorkerError> { - self.store - .set_subjektiv_session_attribution(&self.manifest.worker.name, state)?; - Ok(()) + crate::subjektiv_metadata::compare_and_swap_attribution( + &self.store, + &self.manifest.worker.name, + self.session_id(), + expected, + state, + ) } /// Build an in-process Internal Worker without machine-wide allocation or durable Worker metadata. @@ -8380,6 +8400,7 @@ where feature_resident_summary_source: None, session_context_source: None, prepared_initial_seed: None, + prepared_initial_prompt_completion: None, subjektiv_host: None, restore_feature_lifecycle_pending: false, feature_system_prompt_override: None, @@ -8512,6 +8533,7 @@ where feature_resident_summary_source: None, session_context_source: None, prepared_initial_seed: None, + prepared_initial_prompt_completion: None, subjektiv_host: None, restore_feature_lifecycle_pending: false, feature_system_prompt_override: None, @@ -8923,6 +8945,7 @@ where feature_resident_summary_source: None, session_context_source: None, prepared_initial_seed: None, + prepared_initial_prompt_completion: None, subjektiv_host: None, restore_feature_lifecycle_pending: true, feature_system_prompt_override: None, @@ -11358,9 +11381,9 @@ mod build_summary_prompt_tests { TransportUnknown, } - #[derive(Debug)] struct AttributionLifecycleClient { outcomes: Mutex>, + before_response: Mutex>>, requests: Mutex>, } @@ -11368,11 +11391,18 @@ mod build_summary_prompt_tests { fn new(outcomes: impl IntoIterator) -> Self { Self { outcomes: Mutex::new(outcomes.into_iter().collect()), + before_response: Mutex::new(None), requests: Mutex::new(Vec::new()), } } } + impl std::fmt::Debug for AttributionLifecycleClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("AttributionLifecycleClient") + } + } + impl WorkspaceClient for AttributionLifecycleClient { fn workspace_id(&self) -> Option<&str> { Some("workspace-test") @@ -11391,6 +11421,9 @@ mod build_summary_prompt_tests { request: WorkspaceRequest, ) -> Result { self.requests.lock().unwrap().push(request.clone()); + if let Some(change) = self.before_response.lock().unwrap().take() { + change(); + } match self .outcomes .lock() @@ -11638,6 +11671,110 @@ mod build_summary_prompt_tests { )); } + // Production lifecycle, atomic Host CAS, and filesystem metadata store. Only + // the external Workspace transport is substituted to interleave a newer owner. + #[tokio::test] + async fn delayed_attribution_response_cannot_overwrite_new_session_or_subject_evidence() { + for switch_session in [true, false] { + let client = Arc::new(AttributionLifecycleClient::new([ + AttributionOutcome::Success, + ])); + let (worker, store, _temp) = attribution_lifecycle_worker(client.clone()).await; + let worker_name = worker.manifest().worker.name.clone(); + let replacement_session = if switch_session { + session_store::new_session_id() + } else { + worker.session_id() + }; + let changed = store.clone(); + let name = worker_name.clone(); + *client.before_response.lock().unwrap() = Some(Box::new(move || { + if switch_session { + worker_metadata_writer_for_store(&changed)(WorkerMetadata::new( + name, + Some(WorkerActiveSegmentRef::pending_segment(replacement_session)), + )) + .unwrap(); + } else { + changed + .update_by_name(&name, |metadata| { + metadata.subjektiv_session_attribution = + Some(SubjektivSessionAttributionState::Confirmed { + session_id: replacement_session, + subject_id: "new-subject".into(), + }); + }) + .unwrap(); + } + })); + let error = worker + .finalize_subjektiv_session_attribution( + true, + SubjektivSessionAttributionLifecycle::NewSession, + ) + .unwrap_err(); + assert!( + error.to_string().contains("attribution_conflict"), + "{error}" + ); + let saved = store.read_by_name(&worker_name).unwrap().unwrap(); + assert_eq!( + saved.active.as_ref().unwrap().session_id, + replacement_session + ); + assert_eq!( + saved.subjektiv_session_attribution, + if switch_session { + None + } else { + Some(SubjektivSessionAttributionState::Confirmed { + session_id: replacement_session, + subject_id: "new-subject".into(), + }) + } + ); + assert_eq!(client.requests.lock().unwrap().len(), 1); + } + } + + #[tokio::test] + async fn released_body_cannot_reconfirm_attribution_or_contact_host() { + let client = Arc::new(AttributionLifecycleClient::new([ + AttributionOutcome::Success, + ])); + let (worker, store, _temp) = attribution_lifecycle_worker(client.clone()).await; + let before = store + .read_by_name(&worker.manifest().worker.name) + .unwrap() + .unwrap(); + let control = crate::subject_release::ReleaseControl::new(worker.mutation_authority()); + let request = tokio::spawn({ + let control = control.clone(); + async move { control.request().await } + }); + control.notified().await; + control.respond(|| Ok(())); + let mut reservation = request.await.unwrap().unwrap(); + reservation.commit_disconnect().unwrap(); + drop(reservation); + assert!( + worker + .finalize_subjektiv_session_attribution( + true, + SubjektivSessionAttributionLifecycle::NewSession + ) + .is_err() + ); + assert!(client.requests.lock().unwrap().is_empty()); + assert_eq!( + store + .read_by_name(&worker.manifest().worker.name) + .unwrap() + .unwrap(), + before + ); + } + #[tokio::test] async fn unknown_and_failed_attribution_remain_incomplete_until_lifecycle_retry() { for (first, expected_state) in [ @@ -11750,13 +11887,13 @@ mod build_summary_prompt_tests { ) .unwrap(); store - .set_subjektiv_session_attribution( - &manifest.worker.name, - SubjektivSessionAttributionState::OutcomeUnknown { - session_id, - diagnostic: "response lost before restart".to_string(), - }, - ) + .update_by_name(&manifest.worker.name, |metadata| { + metadata.subjektiv_session_attribution = + Some(SubjektivSessionAttributionState::OutcomeUnknown { + session_id, + diagnostic: "response lost before restart".to_string(), + }); + }) .unwrap(); let client = Arc::new(AttributionLifecycleClient::new([ AttributionOutcome::Success, @@ -17024,6 +17161,110 @@ permission = "write" assert_eq!(worker.history().len(), 1); } + struct PreparedPromptSource { + loads: Arc, + completions: Arc, + } + #[async_trait::async_trait] + impl SystemPromptContributionSource for PreparedPromptSource { + async fn load(&self) -> SystemPromptContribution { + panic!("initial load must use its prepared completion") + } + async fn load_initial( + &self, + ) -> ( + SystemPromptContribution, + Option, + ) { + self.loads.fetch_add(1, Ordering::SeqCst); + let completed = self.completions.clone(); + ( + SystemPromptContribution::Ready("PREPARED_RESIDENT_BODY".into()), + Some(Box::new(move || { + completed.fetch_add(1, Ordering::SeqCst); + })), + ) + } + } + + // Production initial-head preparation/create/reconciliation and real FsStore. + // Only returned write failures and the Feature completion counter are injected. + #[tokio::test] + async fn initial_prompt_completion_waits_for_commit_and_reconciliation_and_never_replays_to_replacement() + { + for failure in [3, 4, 0] { + for replace_source in [false, true] { + let temp = tempfile::tempdir().unwrap(); + let store = FailRunTerminalStore { + inner: session_store::FsStore::new(temp.path().join("sessions")).unwrap(), + fail_run_terminal: Arc::new(AtomicBool::new(false)), + invocation_failure: Arc::new(AtomicUsize::new(failure)), + }; + let mut worker = Worker::new( + minimal_manifest(), + Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient), + store.clone(), + WorkerWorkspaceContext::no_workspace(), + WorkerFilesystemAuthority::None, + Scope::empty(), + ) + .await + .unwrap(); + worker.set_system_prompt_template( + SystemPromptTemplate::parse("default", PromptCatalogSource::builtins_only()) + .unwrap(), + ); + let loads = Arc::new(AtomicUsize::new(0)); + let completed = Arc::new(AtomicUsize::new(0)); + worker.install_system_prompt_contribution( + Some(Arc::new(PreparedPromptSource { + loads: loads.clone(), + completions: completed.clone(), + })), + None, + ); + if failure == 0 { + let fail = Arc::new(AtomicBool::new(true)); + worker.worker_metadata_writer = Some(Arc::new(move |_| { + if fail.swap(false, Ordering::SeqCst) { + Err(std::io::Error::other("initial active pointer write failed").into()) + } else { + Ok(()) + } + })); + } + assert!(worker.materialize_durable_session_head().await.is_err()); + assert_eq!(completed.load(Ordering::SeqCst), 0); + let new_completed = Arc::new(AtomicUsize::new(0)); + let new_loads = Arc::new(AtomicUsize::new(0)); + if replace_source { + worker.install_system_prompt_contribution( + Some(Arc::new(PreparedPromptSource { + loads: new_loads.clone(), + completions: new_completed.clone(), + })), + None, + ); + } + worker.materialize_durable_session_head().await.unwrap(); + worker.materialize_durable_session_head().await.unwrap(); + assert_eq!(loads.load(Ordering::SeqCst), 1); + assert_eq!( + completed.load(Ordering::SeqCst), + usize::from(!replace_source) + ); + assert_eq!(new_completed.load(Ordering::SeqCst), 0); + assert_eq!(new_loads.load(Ordering::SeqCst), 0); + let saved = store + .read_all(worker.session_id(), worker.segment_id()) + .unwrap(); + assert!( + matches!(&saved[0], LogEntry::AnnotatedSegmentStart { system_prompt: Some(prompt), .. } if prompt.contains("PREPARED_RESIDENT_BODY")) + ); + } + } + } + #[tokio::test] async fn initial_metadata_failure_repairs_pointer_without_refetch_or_user_append() { let temp = tempfile::tempdir().unwrap(); From daf1f6ed5089acb8640dca536adc14755ee6e64d Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 03:52:55 +0900 Subject: [PATCH 08/11] fix: route retained Session migration through Runtime staging --- crates/worker-runtime/src/fs_store.rs | 57 +- crates/worker-runtime/src/retention.rs | 112 ++-- .../retention/fixtures/system-input-v4.jsonl | 2 + .../src/retention/runtime_v5_tests.rs | 559 ++++++++++++++++++ 4 files changed, 680 insertions(+), 50 deletions(-) create mode 100644 crates/worker-runtime/src/retention/fixtures/system-input-v4.jsonl create mode 100644 crates/worker-runtime/src/retention/runtime_v5_tests.rs diff --git a/crates/worker-runtime/src/fs_store.rs b/crates/worker-runtime/src/fs_store.rs index 5b9c35b85..de799b323 100644 --- a/crates/worker-runtime/src/fs_store.rs +++ b/crates/worker-runtime/src/fs_store.rs @@ -657,18 +657,32 @@ fn plan_runtime_store_migration( )); } if current_schema_version == SCHEMA_VERSION { + // Runtime and Session schemas evolve independently. Probe only through + // Session Store's non-mutating current-schema admission boundary; any + // conversion still runs on the existing owner-locked staging tree. + let migration_required = crate::retention::sessions_require_migration(root)?; + let excluded_ephemeral_paths = if migration_required { + runtime_tree_exclusions(root)? + } else { + Vec::new() + }; + let archived_orphan_worker_count = if migration_required { + orphaned_worker_directories(root)?.len() + } else { + 0 + }; let plan = FsRuntimeStoreMigrationPlan { current_schema_version, target_schema_version: SCHEMA_VERSION, - migration_required: false, + migration_required, worker_count: 0, - archived_orphan_worker_count: 0, + archived_orphan_worker_count, migrated_worker_aggregate_count: 0, migrated_diagnostic_worker_ref_count: 0, cleared_diagnostic_worker_ref_count: 0, mapping_digest: legacy_worker_identity_mapping_digest(&[]), mappings: Vec::new(), - excluded_ephemeral_paths: Vec::new(), + excluded_ephemeral_paths, }; return Ok((plan, Vec::new())); } @@ -1808,7 +1822,9 @@ fn migrate_runtime_store( Ok(plan) } -fn archive_orphaned_worker_directories(root: &Path) -> Result { +fn orphaned_worker_directories( + root: &Path, +) -> Result, RuntimeError> { let workers_dir = root.join(WORKERS_DIR); let mut orphaned = fs::read_dir(&workers_dir) .map_err(|source| runtime_io_error("read Worker records", &workers_dir, source))? @@ -1821,6 +1837,11 @@ fn archive_orphaned_worker_directories(root: &Path) -> Result>(); orphaned.sort_by(|(left, _), (right, _)| left.cmp(right)); + Ok(orphaned) +} + +fn archive_orphaned_worker_directories(root: &Path) -> Result { + let orphaned = orphaned_worker_directories(root)?; if orphaned.is_empty() { return Ok(0); } @@ -1855,6 +1876,21 @@ fn migrate_runtime_store_in_place( if !plan.migration_required { return Ok(plan); } + if plan.current_schema_version == SCHEMA_VERSION { + // Session-only migration: do not reserialize Runtime/Worker identity, + // Restore guards, recovery fences, or retained execution receipts. + // As in a Runtime schema migration, retain orphan trees outside the + // Worker catalog before validating the completed staging tree. + let archived = archive_orphaned_worker_directories(root)?; + if archived != plan.archived_orphan_worker_count { + return Err(runtime_store_corrupt( + root, + "orphan count changed during Session migration".into(), + )); + } + crate::retention::migrate_staged_sessions(root)?; + return Ok(plan); + } let runtime_path = root.join(RUNTIME_FILE); let bytes = fs::read(&runtime_path).map_err(|error| runtime_io_error("read", &runtime_path, error))?; @@ -1948,11 +1984,9 @@ fn migrate_runtime_store_in_place( ), )); } - // Schema 11 already migrated Sessions; the recovery-fence bump must not - // rewrite retained history or archive attestations. - if plan.current_schema_version != SOURCE_SCHEMA_VERSION { - crate::retention::migrate_staged_sessions(root)?; - } + // A Runtime schema bump says nothing about the retained Session schema. + // Session Store owns versioned conversion, including v4 -> generic v5. + crate::retention::migrate_staged_sessions(root)?; let (document, _) = migrate_runtime_document( document, plan.current_schema_version, @@ -3011,9 +3045,8 @@ mod tests { } else { drop(result.unwrap()); assert_eq!(stored_schema_version(&root), u64::from(SCHEMA_VERSION)); - let manifest: serde_json::Value = - read_json(&session.join("session.json"), "test").unwrap(); - assert_eq!(manifest["schema_version"], 4); + session_store::WorkerSessionStore::open_read_only(&session) + .expect("startup migration must admit the current Session schema"); let once = fs::read(session.join("session.json")).unwrap(); drop(FsRuntimeStore::open_or_create(root.clone(), "runtime-test").unwrap()); assert_eq!(fs::read(session.join("session.json")).unwrap(), once); diff --git a/crates/worker-runtime/src/retention.rs b/crates/worker-runtime/src/retention.rs index 9777e7390..d488c9608 100644 --- a/crates/worker-runtime/src/retention.rs +++ b/crates/worker-runtime/src/retention.rs @@ -1204,9 +1204,13 @@ fn archive_storage( } } -/// Called only on the private Runtime migration staging tree under its owner -/// lock. Failure discards the whole tree, including manifests and attestations. -pub(crate) fn migrate_staged_sessions(root: &Path) -> Result<(), RuntimeError> { +// Share discovery and erasure fencing between the read-only Runtime preflight +// and its private staging pass. Session Store remains the authority for schema +// admission and conversion; Runtime must not duplicate its version table. +fn for_each_live_session( + root: &Path, + mut visit: impl FnMut(&Path, bool) -> Result<(), RuntimeError>, +) -> Result<(), RuntimeError> { for directory in ["workers", "orphaned-workers", "archives/workers"] { let directory = root.join(directory); let entries = match fs::read_dir(&directory) { @@ -1253,44 +1257,72 @@ pub(crate) fn migrate_staged_sessions(root: &Path) -> Result<(), RuntimeError> { Err(session_store::StoreError::SessionErased(_)) => continue, Err(error) => return Err(retention_error(error)), } - let archive = if directory == root.join("archives/workers") { - let manifest: WorkerSessionArchiveManifest = read_json( - &aggregate.join("manifest.json"), - "read archive before Session migration", - )?; - Some(validate_existing_archive(&aggregate, &manifest, false)?) - } else { - None - }; - session_store::WorkerSessionStore::new(&session).map_err(|error| { - RuntimeError::StoreCorrupt { - operation: "migrate Session schema", - path: session.clone(), - message: error.to_string(), - } - })?; - if let Some(ingestion) = archive { - let (checksum_sha256, bytes, file_count) = checksum_files( - &collect_files(&session, "migrate archive storage")?, - "migrate archive storage", - )?; - atomic_write_json( - &aggregate.join(ARCHIVE_STORAGE_FILE), - &MigratedArchiveStorage { - schema_version: 1, - ingestion, - checksum_sha256, - bytes, - file_count, - }, - "commit migrated archive storage", - )?; - } + visit(&aggregate, directory == root.join("archives/workers"))?; } } Ok(()) } +/// Read-only admission probe: a rejected Session needs the existing staged +/// migration attempt (which will also reject corrupt/unsupported inputs). +/// Never create a writable Session store or rewrite archive attestations here. +pub(crate) fn sessions_require_migration(root: &Path) -> Result { + let mut required = false; + for_each_live_session(root, |aggregate, _| { + required |= + session_store::WorkerSessionStore::open_read_only(aggregate.join("session")).is_err(); + Ok(()) + })?; + Ok(required) +} + +/// Called only on the private Runtime migration staging tree under its owner +/// lock. Failure discards the whole tree, including manifests and attestations. +pub(crate) fn migrate_staged_sessions(root: &Path) -> Result<(), RuntimeError> { + for_each_live_session(root, |aggregate, is_archive| { + let session = aggregate.join("session"); + let archive = if is_archive { + let manifest: WorkerSessionArchiveManifest = read_json( + &aggregate.join("manifest.json"), + "read archive before Session migration", + )?; + Some(validate_existing_archive(aggregate, &manifest, false)?) + } else { + None + }; + // Current Sessions need no writable admission, and their storage + // attestations must remain byte-for-byte unchanged. + if session_store::WorkerSessionStore::open_read_only(&session).is_ok() { + return Ok(()); + } + session_store::WorkerSessionStore::new(&session).map_err(|error| { + RuntimeError::StoreCorrupt { + operation: "migrate Session schema", + path: session.clone(), + message: error.to_string(), + } + })?; + if let Some(ingestion) = archive { + let (checksum_sha256, bytes, file_count) = checksum_files( + &collect_files(&session, "migrate archive storage")?, + "migrate archive storage", + )?; + atomic_write_json( + &aggregate.join(ARCHIVE_STORAGE_FILE), + &MigratedArchiveStorage { + schema_version: 1, + ingestion, + checksum_sha256, + bytes, + file_count, + }, + "commit migrated archive storage", + )?; + } + Ok(()) + }) +} + fn validate_existing_archive( archive_dir: &Path, expected: &WorkerSessionArchiveManifest, @@ -1753,12 +1785,16 @@ fn temporary_path(path: &Path) -> PathBuf { path.with_file_name(format!(".{name}.tmp-{}-{sequence}", std::process::id())) } +#[cfg(all(test, feature = "fs-store"))] +#[path = "retention/runtime_v5_tests.rs"] +mod runtime_v5_tests; + #[cfg(test)] mod tests { use super::*; use std::sync::{Arc, Barrier}; - fn write_json(path: &Path, value: &impl Serialize) { + pub(super) fn write_json(path: &Path, value: &impl Serialize) { fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, serde_json::to_vec_pretty(value).unwrap()).unwrap(); } @@ -1786,7 +1822,7 @@ mod tests { fs::write(worker.join("runs/attempt-a/worker.sock"), b"not retained").unwrap(); } - fn request( + pub(super) fn request( worker_id: WorkerId, disposition: SessionDisposition, ) -> WorkerRetentionExecutionRequest { diff --git a/crates/worker-runtime/src/retention/fixtures/system-input-v4.jsonl b/crates/worker-runtime/src/retention/fixtures/system-input-v4.jsonl new file mode 100644 index 000000000..00a1557f2 --- /dev/null +++ b/crates/worker-runtime/src/retention/fixtures/system-input-v4.jsonl @@ -0,0 +1,2 @@ +{"kind":"annotated_system_item","ts":101,"entry":{"item":{"kind":"journal_startup","body":"journal body\nunchanged","snapshot":{"entries":[{"text":"private snapshot"}]},"snapshot_sha256":"snapshot-digest","prompt_provenance":{"workspace_id":"workspace","source_digest":"source","projection_digest":"projection","logical_name":"journal_startup"}},"metadata":{"entry_id":"journal-id","origin":{"kind":"legacy_unknown"},"derivation":{"sources":["source-id"]}}},"extensions":[{"domain":"migration-fixture","payload":{"kept":true}}]} +{"kind":"annotated_system_item","ts":103,"entry":{"item":{"kind":"subject_behavior_refresh","body":"rendered subject body","behavior_md":"exact behavior","prompt_provenance":{"source_digest":"subject-source","projection_digest":"subject-projection","logical_name":"subject_behavior"}},"metadata":{"entry_id":"subject-id","origin":{"kind":"backend_instruction","operation_id":"op"}}}} diff --git a/crates/worker-runtime/src/retention/runtime_v5_tests.rs b/crates/worker-runtime/src/retention/runtime_v5_tests.rs new file mode 100644 index 000000000..8c0123606 --- /dev/null +++ b/crates/worker-runtime/src/retention/runtime_v5_tests.rs @@ -0,0 +1,559 @@ +//! Runtime integration, not a second implementation of Session migration. +//! The Feature records are frozen v4 wire copied from the Session Store fixture; +//! only the empty segment envelope is built with current APIs. +use super::tests::{request, write_json}; +use super::*; +use crate::fs_store::{FsRuntimeStore, FsRuntimeStoreOptions}; +use serde_json::{Value, json}; +use session_store::{LogEntry, SegmentId, SessionId, Store, WorkerSessionStore}; +use std::collections::BTreeMap; + +const V4_SYSTEM_INPUTS: &str = include_str!("fixtures/system-input-v4.jsonl"); + +fn write_v4_session(aggregate: &Path) -> (SessionId, SegmentId) { + let root = aggregate.join("session"); + let session = session_store::new_session_id(); + let segment = session_store::new_segment_id(); + let store = WorkerSessionStore::new(&root).unwrap(); + store + .create_segment( + session, + segment, + &[LogEntry::AnnotatedSegmentStart { + ts: 1, + session_id: session, + system_prompt: None, + config: Default::default(), + history: vec![], + forked_from: None, + compacted_from: None, + }], + ) + .unwrap(); + let path = root.join("segments").join(format!("{segment}.jsonl")); + let mut bytes = fs::read(&path).unwrap(); + bytes.extend_from_slice(V4_SYSTEM_INPUTS.as_bytes()); + fs::write(path, bytes).unwrap(); + write_json( + &root.join("session.json"), + &json!({ + "schema_version": 4, "session_id": session, + }), + ); + (session, segment) +} + +fn stopped_worker(root: &Path, schema: u32) -> (WorkerId, SessionId, SegmentId) { + let worker = WorkerId::now_v7(); + let aggregate = root.join("workers").join(worker.to_string()); + // Serialize request defaults just as a production aggregate writer does. + let request: crate::catalog::CreateWorkerRequest = serde_json::from_value(json!({ + "worker_id": worker, + "workspace_api": {"workspace_id": "workspace-a", "base_url": "https://workspace.invalid"}, + "create_fingerprint": "runtime-v5-fixture", + "profile": {"kind": "builtin", "value": "builtin:coder"}, + "profile_source": {"kind": "workspace_config", "archive": { + "id": "archive-1", "digest": "sha256:archive", "size_bytes": 0, + "source_graph": {"source_count": 0, "total_source_bytes": 0, + "entrypoints": {}, "import_count": 0} + }} + })) + .unwrap(); + write_json( + &aggregate.join("worker.json"), + &json!({ + "schema_version": schema, + "worker_ref": {"worker_id": worker}, "worker_id": worker, + "profile": request.profile, + "profile_source": request.profile_source.reference(), + "display_name": null, "config_bundle": null, + "created_at_ms": 1234, "status": "stopped", "workspace_id": "workspace-a", + "last_finished_submission_request_id": "retained-submission", + "restore_guard": {"observation_token": "retained-token", + "active_request_id": null, "owners": {}}, + "execution_state": {"state": "available", "execution": { + "request": request, "binding": {}, "restore_intent": "explicit" + }} + }), + ); + let (session, segment) = write_v4_session(&aggregate); + let mut metadata = session_store::WorkerMetadata::new( + format!("worker-runtime-{worker}"), + Some(session_store::WorkerActiveSegmentRef::active_segment( + session, segment, + )), + ); + metadata.workspace_id = Some("workspace-a".into()); + write_json(&aggregate.join("metadata.json"), &metadata); + (worker, session, segment) +} + +fn runtime_fixture(root: &Path, schema: u32) -> (WorkerId, SessionId, SegmentId) { + fs::create_dir_all(root.join("workers")).unwrap(); + let fenced_worker = WorkerId::now_v7(); + write_json( + &root.join("runtime.json"), + &json!({ + "schema_version": schema, "display_name": "retained Runtime", + "backend": "fs_store", "status": "running", "next_diagnostic_id": 1, + "config_bundles": {}, "workspace_owners": {"workspace-a": "owner-a"}, + "diagnostics": [], "subject_releases": [], + "subject_create_recovery_fences": {fenced_worker.to_string(): { + "request": {"workspace_id": "workspace-a", "worker_id": fenced_worker, + "create_fingerprint": "retained-create", "operation_id": "retained-recovery"}, + "response": null + }} + }), + ); + stopped_worker(root, schema) +} + +// Include every file, not just schema markers: failures must preserve original +// history, metadata, archive receipts, checksums and retention evidence together. +fn tree_bytes(root: &Path) -> BTreeMap> { + fn walk(root: &Path, at: &Path, result: &mut BTreeMap>) { + for entry in fs::read_dir(at).unwrap() { + let entry = entry.unwrap(); + if entry.file_type().unwrap().is_dir() { + walk(root, &entry.path(), result); + } else { + result.insert( + entry.path().strip_prefix(root).unwrap().to_path_buf(), + fs::read(entry.path()).unwrap(), + ); + } + } + } + let mut result = BTreeMap::new(); + walk(root, root, &mut result); + result +} + +fn assert_generic(root: &Path, session: SessionId, segment: SegmentId) { + use session_store::{InputCompactionUse, InputEvidenceUse, InputObservationUse}; + let store = WorkerSessionStore::open_read_only(root).unwrap(); + let entries = store.read_all_read_only(session, segment).unwrap(); + assert_eq!(entries.len(), 3); + let originals: Vec = V4_SYSTEM_INPUTS + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect(); + for (log, old) in entries[1..].iter().zip(originals) { + let LogEntry::AnnotatedSystemItem { entry, .. } = log else { + panic!("lost System record: {log:?}"); + }; + let current = serde_json::to_value(log).unwrap(); + assert_eq!(current["ts"], old["ts"]); + for field in ["entry_id", "origin", "derivation"] { + assert_eq!( + current["entry"]["metadata"][field], + old["entry"]["metadata"][field] + ); + } + assert_eq!(current["entry"]["item"]["kind"], "input"); + assert_eq!( + entry.item.history_text(), + old["entry"]["item"]["body"].as_str().unwrap() + ); + let input = entry.metadata.system_input.as_ref().unwrap(); + let usage = input.usage(); + assert_eq!(usage.recording_material, InputEvidenceUse::Exclude); + assert_eq!(usage.memory_evidence, InputEvidenceUse::Exclude); + assert_eq!(usage.observation, InputObservationUse::Hidden); + let kind = old["entry"]["item"]["kind"].as_str().unwrap(); + let schema = if kind == "journal_startup" { + assert_eq!(usage.compaction, InputCompactionUse::ReferenceInput); + assert_eq!(current["extensions"], old["extensions"]); + "journal:startup" + } else { + assert_eq!(usage.compaction, InputCompactionUse::Retain); + "subject:behavior" + }; + let payload = input.feature_payload().unwrap(); + assert_eq!(payload.schema(), schema); + assert_eq!(payload.version(), 1); + let mut expected_payload = old["entry"]["item"].clone(); + expected_payload.as_object_mut().unwrap().remove("kind"); + expected_payload.as_object_mut().unwrap().remove("body"); + assert_eq!( + payload.decode::(schema, 1).unwrap(), + expected_payload + ); + let public = serde_json::to_value(input.public_item().unwrap()).unwrap(); + assert_eq!(public["kind"], kind); + assert_eq!(public["body"], old["entry"]["item"]["body"]); + assert_eq!(public["presentation"], json!({"kind": "context"})); + } +} + +fn completed_v4_archive( + root: &Path, + schema: u32, +) -> ( + SessionId, + SegmentId, + WorkerRetentionExecutionRequest, + WorkerRetentionExecutionResult, +) { + let (worker, session, segment) = stopped_worker(root, schema); + let provider = FsWorkerRetentionProvider::new(root); + let request = request(worker, SessionDisposition::Archive); + // Frozen old-runtime receipt: ingestion and removal completed before the + // current strict decoder existed. Do not ask the v5 reader to mint it. + let manifest = + commit_session_archive(&provider, &request, &provider.worker_dir(worker)).unwrap(); + let completed = WorkerRetentionExecutionResult { + operation_id: request.operation_id.clone(), + input_fingerprint: request.input_fingerprint.clone(), + worker_id: worker, + session_disposition: SessionDisposition::Archive, + diagnostics_disposition: DiagnosticsDisposition::Purge, + archive: Some(manifest), + source_removed: true, + diagnostics_retained: false, + }; + write_json( + &provider.operation_path(&request.operation_id).unwrap(), + &RetentionOperationReceipt { + schema_version: OPERATION_SCHEMA_VERSION, + request: request.clone(), + result: completed.clone(), + }, + ); + fs::remove_dir_all(provider.worker_dir(worker)).unwrap(); + (session, segment, request, completed) +} + +#[test] +fn runtime_v11_and_current_route_v4_stopped_orphan_archive_through_staging() { + for runtime_schema in [11, 12] { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("runtime"); + let (worker, session, segment) = runtime_fixture(&root, runtime_schema); + let orphan = root + .join("orphaned-workers") + .join(WorkerId::now_v7().to_string()); + let (orphan_session, orphan_segment) = write_v4_session(&orphan); + // Include an unregistered tree still under workers, not just an + // already-archived orphan. Both Runtime source schemas must fence it + // out of the live catalog without discarding its Session. + let loose_id = WorkerId::now_v7(); + let loose = root.join("workers").join(loose_id.to_string()); + let (loose_session, loose_segment) = write_v4_session(&loose); + let retained_loose = root.join("orphaned-workers").join(loose_id.to_string()); + let (archived_session, archived_segment, request, completed) = + completed_v4_archive(&root, runtime_schema); + let provider = FsWorkerRetentionProvider::new(&root); + let archive = provider.archive_dir("archive-a").unwrap(); + // Also cover an archive which already has a local storage attestation + // from an earlier schema migration. Keep ingestion bytes immutable; + // harmless marker whitespace makes current storage differ from them. + if runtime_schema == 12 { + let marker = archive.join("session/session.json"); + let mut bytes = fs::read(&marker).unwrap(); + bytes.push(b'\n'); + fs::write(marker, bytes).unwrap(); + let ingestion = completed.archive.clone().unwrap(); + let (checksum_sha256, bytes, file_count) = checksum_files( + &collect_files(&archive.join("session"), "test").unwrap(), + "test", + ) + .unwrap(); + write_json( + &archive.join(ARCHIVE_STORAGE_FILE), + &MigratedArchiveStorage { + schema_version: 1, + ingestion, + checksum_sha256, + bytes, + file_count, + }, + ); + } + let storage_before = fs::read(archive.join(ARCHIVE_STORAGE_FILE)).ok(); + let manifest_before = fs::read(archive.join("manifest.json")).unwrap(); + let receipt_path = provider.operation_path(&request.operation_id).unwrap(); + let receipt_before = fs::read(&receipt_path).unwrap(); + let stopped = root.join("workers").join(worker.to_string()); + assert!(WorkerSessionStore::open_read_only(stopped.join("session")).is_err()); + // Current decode itself must not be made permissive to fix routing. + for line in V4_SYSTEM_INPUTS.lines() { + assert!(serde_json::from_str::(line).is_err()); + } + let before = tree_bytes(&root); + let options = FsRuntimeStoreOptions::new(&root).with_runtime_id("runtime-a"); + let plan = FsRuntimeStore::migration_plan(&options).unwrap(); + assert!(plan.migration_required); + assert_eq!(plan.archived_orphan_worker_count, 1); + assert_eq!(plan.current_schema_version, runtime_schema); + assert_eq!(tree_bytes(&root), before, "preflight mutated source"); + // Exercise normal startup, not just the private migration helper. + let opened = FsRuntimeStore::open_or_create(root.clone(), "runtime-a").unwrap(); + assert!( + matches!( + FsRuntimeStore::migrate(&options), + Err(RuntimeError::RuntimeStoreAlreadyOpen { .. }) + ), + "owner lock not fenced" + ); + drop(opened); + assert_generic(&stopped.join("session"), session, segment); + assert_generic(&orphan.join("session"), orphan_session, orphan_segment); + assert!(!loose.exists()); + assert_generic( + &retained_loose.join("session"), + loose_session, + loose_segment, + ); + assert_generic(&archive.join("session"), archived_session, archived_segment); + assert_eq!( + fs::read(archive.join("manifest.json")).unwrap(), + manifest_before + ); + assert_eq!(fs::read(&receipt_path).unwrap(), receipt_before); + let (manifest, archive_session) = provider.session_archive("archive-a").unwrap(); + assert_eq!(completed.archive.as_ref(), Some(&manifest)); + assert_eq!(provider.execute(&request).unwrap(), completed); + if let Some(storage_before) = storage_before { + assert_ne!( + fs::read(archive.join(ARCHIVE_STORAGE_FILE)).unwrap(), + storage_before + ); + } + let storage: MigratedArchiveStorage = + read_json(&archive.join(ARCHIVE_STORAGE_FILE), "test").unwrap(); + assert_eq!(storage.ingestion, manifest); + let (checksum, bytes, count) = + checksum_files(&collect_files(&archive_session, "test").unwrap(), "test").unwrap(); + assert_eq!( + (storage.checksum_sha256, storage.bytes, storage.file_count), + (checksum, bytes, count) + ); + if runtime_schema == 12 { + for name in [ + PathBuf::from("runtime.json"), + PathBuf::from(format!("workers/{worker}/worker.json")), + PathBuf::from(format!("workers/{worker}/metadata.json")), + ] { + assert_eq!(fs::read(root.join(&name)).unwrap(), before[&name]); + } + } + let once = tree_bytes(&root); + assert!( + !FsRuntimeStore::migrate(&options) + .unwrap() + .migration_required + ); + drop(FsRuntimeStore::open_or_create(root.clone(), "runtime-a").unwrap()); + assert_eq!(tree_bytes(&root), once, "retry rewrote migrated evidence"); + assert!(!temp.path().join(".runtime.schema-v12-staging").exists()); + assert!(!temp.path().join(".runtime.pre-schema-v12-backup").exists()); + // session-storage, not the immutable receipt's old checksum, attests + // current bytes. Tampering with those bytes must remain detectable. + let path = archive_session + .join("segments") + .join(format!("{archived_segment}.jsonl")); + fs::write(path, b"tampered\n").unwrap(); + assert!(provider.session_archive("archive-a").is_err()); + } +} + +#[test] +fn runtime_v5_archive_failure_discards_staging_without_losing_originals_and_retry_converges() { + for runtime_schema in [11, 12] { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("runtime"); + let (worker, session, segment) = runtime_fixture(&root, runtime_schema); + let (archived_session, archived_segment, request, completed) = + completed_v4_archive(&root, runtime_schema); + let provider = FsWorkerRetentionProvider::new(&root); + let archive = provider.archive_dir("archive-a").unwrap(); + let path = archive + .join("session/segments") + .join(format!("{archived_segment}.jsonl")); + let original = fs::read(&path).unwrap(); + fs::write(&path, b"corrupt archive\n").unwrap(); + let before = tree_bytes(&root); + let options = FsRuntimeStoreOptions::new(&root).with_runtime_id("runtime-a"); + assert!( + FsRuntimeStore::migration_plan(&options) + .unwrap() + .migration_required + ); + // workers are visited before archives: earlier successful staged + // conversion must not leak into the original tree on this later error. + assert!(FsRuntimeStore::open_or_create(root.clone(), "runtime-a").is_err()); + assert_eq!(tree_bytes(&root), before); + assert!(!archive.join(ARCHIVE_STORAGE_FILE).exists()); + fs::write(&path, original).unwrap(); + FsRuntimeStore::migrate(&options).unwrap(); + assert_generic( + &root + .join("workers") + .join(worker.to_string()) + .join("session"), + session, + segment, + ); + assert_generic(&archive.join("session"), archived_session, archived_segment); + assert_eq!(provider.execute(&request).unwrap(), completed); + } +} + +#[test] +fn runtime_v5_typed_legacy_payload_failure_keeps_valid_archive_evidence_on_retries() { + for (from, to) in [ + ( + "\"snapshot_sha256\":\"snapshot-digest\"", + "\"snapshot_sha256\":42", + ), + ("\"behavior_md\":\"exact behavior\"", "\"behavior_md\":[]"), + ] { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("runtime"); + runtime_fixture(&root, 12); + let (worker, _, segment) = stopped_worker(&root, 12); + let provider = FsWorkerRetentionProvider::new(&root); + let path = provider + .worker_dir(worker) + .join("session/segments") + .join(format!("{segment}.jsonl")); + let original = fs::read_to_string(&path).unwrap(); + assert!(original.contains(from)); + fs::write(&path, original.replace(from, to)).unwrap(); + let request = request(worker, SessionDisposition::Archive); + let manifest = + commit_session_archive(&provider, &request, &provider.worker_dir(worker)).unwrap(); + fs::remove_dir_all(provider.worker_dir(worker)).unwrap(); + let archive = provider.archive_dir("archive-a").unwrap(); + // This archive checksum is valid; failure must come from the legacy + // typed decoder, not from deliberately broken ingestion evidence. + validate_existing_archive(&archive, &manifest, false).unwrap(); + let before = tree_bytes(&root); + let options = FsRuntimeStoreOptions::new(&root).with_runtime_id("runtime-a"); + for _ in 0..2 { + let error = FsRuntimeStore::migrate(&options).unwrap_err(); + let RuntimeError::StoreCorrupt { + operation, path, .. + } = error + else { + panic!("unexpected failure: {error}"); + }; + assert_eq!(operation, "migrate Session schema"); + assert!(path.starts_with(temp.path().join(".runtime.schema-v12-staging"))); + assert_eq!(tree_bytes(&root), before); + validate_existing_archive(&archive, &manifest, false).unwrap(); + } + } +} + +#[test] +fn runtime_v5_probe_and_staging_skip_erased_originals() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("runtime"); + let (worker, live_session, live_segment) = runtime_fixture(&root, 12); + let erased = root + .join("orphaned-workers") + .join(WorkerId::now_v7().to_string()); + let (session, segment) = write_v4_session(&erased); + runtime_session_retention(&root) + .delete( + session, + session_store::retention::SessionDeletionIntent::ErasePermanently, + || Ok(()), + ) + .unwrap(); + fs::write( + erased + .join("session/segments") + .join(format!("{segment}.jsonl")), + b"fenced malformed JSON\n", + ) + .unwrap(); + let before = tree_bytes(&erased); + let options = FsRuntimeStoreOptions::new(&root).with_runtime_id("runtime-a"); + assert!( + FsRuntimeStore::migration_plan(&options) + .unwrap() + .migration_required + ); + FsRuntimeStore::migrate(&options).unwrap(); + assert_generic( + &root + .join("workers") + .join(worker.to_string()) + .join("session"), + live_session, + live_segment, + ); + assert_eq!(tree_bytes(&erased), before); + assert!( + !FsRuntimeStore::migration_plan(&options) + .unwrap() + .migration_required + ); + assert!(matches!( + runtime_session_retention(&root).ensure_live(session), + Err(session_store::StoreError::SessionErased(_)) + )); +} + +#[test] +fn runtime_v5_rejects_broken_or_future_source_without_rewriting_evidence() { + for corruption in ["broken-json", "future-schema"] { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("runtime"); + let (worker, session, segment) = runtime_fixture(&root, 12); + let session_root = root + .join("workers") + .join(worker.to_string()) + .join("session"); + if corruption == "broken-json" { + fs::write( + session_root + .join("segments") + .join(format!("{segment}.jsonl")), + b"{not valid JSON\n", + ) + .unwrap(); + } else { + write_json( + &session_root.join("session.json"), + &json!({ + "schema_version": 999, "session_id": session, + }), + ); + } + let before = tree_bytes(&root); + let options = FsRuntimeStoreOptions::new(&root).with_runtime_id("runtime-a"); + assert!( + FsRuntimeStore::migration_plan(&options) + .unwrap() + .migration_required + ); + assert_eq!(tree_bytes(&root), before); + assert!(FsRuntimeStore::migrate(&options).is_err()); + assert_eq!(tree_bytes(&root), before); + assert!(!temp.path().join(".runtime.schema-v12-staging").exists()); + assert!(!temp.path().join(".runtime.pre-schema-v12-backup").exists()); + } +} + +#[cfg(unix)] +#[test] +fn current_runtime_session_migration_still_rejects_live_worker_sockets() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("runtime"); + let (worker, _, _) = runtime_fixture(&root, 12); + let before = tree_bytes(&root); + let runs = root.join("workers").join(worker.to_string()).join("runs"); + fs::create_dir_all(runs.join("1")).unwrap(); + let listener = std::os::unix::net::UnixListener::bind(runs.join("1/worker.sock")).unwrap(); + let options = FsRuntimeStoreOptions::new(&root).with_runtime_id("runtime-a"); + let error = FsRuntimeStore::migrate(&options).unwrap_err(); + assert!(error.to_string().contains("active Worker socket")); + drop(listener); + fs::remove_dir_all(runs).unwrap(); + assert_eq!(tree_bytes(&root), before); + assert!(!temp.path().join(".runtime.schema-v12-staging").exists()); +} From 8f759911abb35dd4e62eb2c1ad3c6aa6accb6b32 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 03:52:55 +0900 Subject: [PATCH 09/11] fix: emit Session use policy type in browser bindings --- crates/protocol/src/typescript.rs | 13 +++++++------ web/workspace/src/lib/generated/protocol.ts | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/protocol/src/typescript.rs b/crates/protocol/src/typescript.rs index d2fc61d91..192cf2a41 100644 --- a/crates/protocol/src/typescript.rs +++ b/crates/protocol/src/typescript.rs @@ -17,12 +17,12 @@ use crate::{ 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, - WorkerBusyState, WorkerCommandAcknowledgement, WorkerCommandDisposition, WorkerCommandEnvelope, - WorkerCommandKind, WorkerEvent, WorkerMaintenanceState, WorkerRunState, WorkerState, - WorkerStateSnapshot, WorkerStatus, + SessionEntryUsePolicy, SessionHistoryLineageBoundary, SessionHistoryPage, SessionMessageRole, + SessionSnapshot, SessionSnapshotEntry, SessionSnapshotEntryData, SessionToolAttachment, + SubmissionDisposition, SymlinkPolicy, ToolResultDisposition, TurnResult, + UploadedFileAvailability, UploadedFileRef, WorkerBusyState, WorkerCommandAcknowledgement, + WorkerCommandDisposition, WorkerCommandEnvelope, WorkerCommandKind, WorkerEvent, + WorkerMaintenanceState, WorkerRunState, WorkerState, WorkerStateSnapshot, WorkerStatus, subscription::{ EventSubscriptionSelector, MAX_CORRELATION_ID_BYTES, MAX_REJECTION_MESSAGE_BYTES, MAX_RESOURCE_ID_BYTES, MAX_SUBSCRIPTION_COLLECTION_ITEMS, @@ -117,6 +117,7 @@ pub fn generated_protocol_types() -> String { push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); push_decl::(&cfg, &mut output); diff --git a/web/workspace/src/lib/generated/protocol.ts b/web/workspace/src/lib/generated/protocol.ts index 8223baf07..8b252d488 100644 --- a/web/workspace/src/lib/generated/protocol.ts +++ b/web/workspace/src/lib/generated/protocol.ts @@ -178,6 +178,8 @@ compaction?: InFlightCompaction | null, }; export type SessionEntryProvenance = "human_input" | "worker_input" | "flow_instruction" | "backend_instruction" | "model_output" | "tool_output" | "derived_summary" | "legacy_unknown"; +export type SessionEntryUsePolicy = { observation_readable: boolean, memory_evidence: boolean, }; + export type SessionMessageRole = "user" | "assistant"; export type SessionContentPart = { "kind": "text", text: string, } | { "kind": "refusal", refusal: string, }; From 3b3381900f9141d401be6312183df05f975e6c5d Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 04:45:50 +0900 Subject: [PATCH 10/11] fix: preserve role-neutral input policy across compaction and replay --- crates/session-store/src/fs_store.rs | 4 +- crates/session-store/src/history.rs | 92 +++++---- .../{system_input.rs => input_metadata.rs} | 115 ++++++------ .../session-store/src/legacy_session_log.rs | 116 +++++++++--- .../src/legacy_session_log/tests.rs | 2 +- .../tests/fixtures/v5-role-metadata.jsonl | 5 + .../src/legacy_session_log/tests/v5.rs | 137 ++++++++++++-- crates/session-store/src/lib.rs | 10 +- crates/session-store/src/public_index.rs | 4 +- crates/session-store/src/public_material.rs | 11 +- .../src/public_material/tests.rs | 32 ++-- crates/session-store/src/public_snapshot.rs | 35 ++-- crates/session-store/src/segment_log.rs | 10 +- crates/session-store/src/store.rs | 4 +- .../session-store/src/worker_session_store.rs | 29 +-- crates/session-store/tests/fs_store_test.rs | 17 +- crates/session-store/tests/session_test.rs | 2 +- .../standalone/src/subjektiv/journal/tests.rs | 2 +- .../src/subjektiv/journal/tests/compaction.rs | 27 +++ .../journal/tests/connected_compaction.rs | 6 +- .../src/subjektiv/journal/tests/handoff.rs | 2 +- crates/standalone/src/subjektiv_sessions.rs | 2 +- crates/tui/src/app.rs | 2 +- .../tests/session_material_tests.rs | 2 +- crates/worker-runtime/src/retention.rs | 4 +- .../src/retention/runtime_v5_tests.rs | 77 +++++++- crates/worker-runtime/src/session_public.rs | 2 +- crates/worker-runtime/src/worker_backend.rs | 2 +- crates/worker/src/feature/builtin/journal.rs | 4 +- crates/worker/src/feature/builtin/memory.rs | 12 +- crates/worker/src/feature/invocation.rs | 6 +- crates/worker/src/internal_worker.rs | 2 +- crates/worker/src/ipc/interceptor.rs | 2 +- crates/worker/src/session_capture.rs | 176 ++++++++++++++++-- crates/worker/src/session_history.rs | 10 +- crates/worker/src/skill.rs | 6 +- crates/worker/src/spawn/registry.rs | 2 +- crates/worker/src/subjektiv.rs | 6 +- crates/worker/src/wip.rs | 2 +- crates/worker/src/worker.rs | 51 +++-- crates/worker/tests/compact_events_test.rs | 14 +- .../worker/tests/feature_invocation_test.rs | 2 +- crates/workspace-server/src/server.rs | 38 ++-- .../subjektiv_journal_connected_tests.rs | 2 +- .../server/tests/subjektiv_journal_tests.rs | 4 +- .../tests/subjektiv_memory_journal_tests.rs | 2 +- 46 files changed, 779 insertions(+), 315 deletions(-) rename crates/session-store/src/{system_input.rs => input_metadata.rs} (82%) create mode 100644 crates/session-store/src/legacy_session_log/tests/fixtures/v5-role-metadata.jsonl diff --git a/crates/session-store/src/fs_store.rs b/crates/session-store/src/fs_store.rs index aa9988e8f..26226a5fc 100644 --- a/crates/session-store/src/fs_store.rs +++ b/crates/session-store/src/fs_store.rs @@ -361,7 +361,7 @@ impl Store for FsStore { segment_id: SegmentId, entry: &LogEntry, ) -> Result<(), StoreError> { - entry.validate_system_inputs()?; + entry.validate_input_metadata()?; let _retention = self.retention().lock_live(session_id)?; let line = serde_json::to_string(entry)?; self.append_line(&self.log_path(session_id, segment_id), &line) @@ -450,7 +450,7 @@ impl Store for FsStore { entries: &[LogEntry], ) -> Result<(), StoreError> { for entry in entries { - entry.validate_system_inputs()?; + entry.validate_input_metadata()?; } let _retention = self.retention().lock_live(session_id)?; let path = self.log_path(session_id, segment_id); diff --git a/crates/session-store/src/history.rs b/crates/session-store/src/history.rs index 67d753c13..4170cdf60 100644 --- a/crates/session-store/src/history.rs +++ b/crates/session-store/src/history.rs @@ -85,21 +85,22 @@ pub struct LoggedHistoryDerivation { } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct LoggedSessionHistoryMetadata { pub entry_id: LoggedSessionHistoryEntryId, pub origin: LoggedSessionHistoryOrigin, #[serde(default, skip_serializing_if = "Option::is_none")] pub derivation: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub system_input: Option, + pub input_metadata: Option, } impl LoggedSessionHistoryMetadata { - pub(crate) fn validate_system_input(&self) -> Result<(), crate::SystemInputError> { + pub(crate) fn validate_input_metadata(&self) -> Result<(), crate::InputMetadataError> { if matches!(self.origin, LoggedSessionHistoryOrigin::Provided { .. }) - && self.system_input.is_none() + && self.input_metadata.is_none() { - return Err(crate::SystemInputError::MissingMetadata); + return Err(crate::InputMetadataError::MissingMetadata); } Ok(()) } @@ -109,7 +110,7 @@ impl LoggedSessionHistoryMetadata { entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, - system_input: None, + input_metadata: None, } } } @@ -131,34 +132,40 @@ struct HistoryEntryWire { } impl TryFrom for LoggedHistoryEntry { - type Error = crate::SystemInputError; + type Error = crate::InputMetadataError; fn try_from(wire: HistoryEntryWire) -> Result { let entry = Self { item: wire.item, metadata: wire.metadata, }; - entry.validate_system_input()?; + entry.validate_input_metadata()?; Ok(entry) } } impl LoggedHistoryEntry { - pub fn validate_system_input(&self) -> Result<(), crate::SystemInputError> { - self.metadata.validate_system_input()?; - if self.metadata.system_input.is_some() { - let LoggedItem::Message { - role: crate::LoggedRole::System, - content, - } = &self.item - else { - return Err(crate::SystemInputError::InconsistentRecord); - }; - let [crate::LoggedContentPart::Text { text }] = content.as_slice() else { - return Err(crate::SystemInputError::InconsistentRecord); - }; - if text.len() > crate::system_input::MAX_SYSTEM_INPUT_BYTES { - return Err(crate::SystemInputError::ResourceLimit); + pub fn validate_input_metadata(&self) -> Result<(), crate::InputMetadataError> { + self.metadata.validate_input_metadata()?; + if let Some(input) = &self.metadata.input_metadata { + match &self.item { + LoggedItem::Message { + role: crate::LoggedRole::System, + content, + } => { + let [crate::LoggedContentPart::Text { text }] = content.as_slice() else { + return Err(crate::InputMetadataError::InconsistentRecord); + }; + if text.len() > crate::input_metadata::MAX_SYSTEM_INPUT_BYTES { + return Err(crate::InputMetadataError::ResourceLimit); + } + } + // Other logical roles may carry use policy, but cannot masquerade + // as the System-only public/payload extension contract. + _ if input.feature_payload().is_some() || input.public_item().is_some() => { + return Err(crate::InputMetadataError::InconsistentRecord); + } + _ => {} } } Ok(()) @@ -183,14 +190,14 @@ struct SystemEntryWire { } impl TryFrom for LoggedSystemHistoryEntry { - type Error = crate::SystemInputError; + type Error = crate::InputMetadataError; fn try_from(wire: SystemEntryWire) -> Result { let entry = Self { item: wire.item, metadata: wire.metadata, }; - entry.validate_system_input()?; + entry.validate_input_metadata()?; Ok(entry) } } @@ -201,30 +208,30 @@ impl LoggedSystemHistoryEntry { derivation: Option, input: crate::SystemInputEnvelope, ) -> Self { - let (body, system_input) = input.into_parts(); + let (body, input_metadata) = input.into_parts(); Self { item: crate::SystemItem::Input { body }, metadata: LoggedSessionHistoryMetadata { entry_id: LoggedSessionHistoryEntryId::new(), origin, derivation, - system_input: Some(system_input), + input_metadata: Some(input_metadata), }, } } - pub fn validate_system_input(&self) -> Result<(), crate::SystemInputError> { - self.metadata.validate_system_input()?; - match (&self.item, &self.metadata.system_input) { + pub fn validate_input_metadata(&self) -> Result<(), crate::InputMetadataError> { + self.metadata.validate_input_metadata()?; + match (&self.item, &self.metadata.input_metadata) { (crate::SystemItem::Input { body }, Some(_)) => { - if body.len() > crate::system_input::MAX_SYSTEM_INPUT_BYTES { - return Err(crate::SystemInputError::ResourceLimit); + if body.len() > crate::input_metadata::MAX_SYSTEM_INPUT_BYTES { + return Err(crate::InputMetadataError::ResourceLimit); } } (crate::SystemItem::Input { .. }, None) => { - return Err(crate::SystemInputError::MissingMetadata); + return Err(crate::InputMetadataError::MissingMetadata); } - (_, Some(_)) => return Err(crate::SystemInputError::InconsistentRecord), + (_, Some(_)) => return Err(crate::InputMetadataError::InconsistentRecord), (_, None) => {} } Ok(()) @@ -241,9 +248,9 @@ impl LoggedSystemHistoryEntry { if matches!(self.item, crate::SystemItem::Input { .. }) { return self .metadata - .system_input + .input_metadata .as_ref() - .and_then(crate::SystemInputMetadata::public_item) + .and_then(crate::HistoryInputMetadata::public_item) .cloned() .unwrap_or_else(|| self.item.public_item()); } @@ -268,7 +275,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".into(), @@ -293,7 +300,7 @@ mod tests { "item": { "kind": "input", "body": "context" }, "metadata": { "entry_id": "stable-entry", "origin": { "kind": "provided", "producer": "owner" }, - "system_input": { "usage": { + "input_metadata": { "usage": { "recording_material": "exclude", "memory_evidence": "exclude", "observation": "hidden", "compaction": "reference_input" } } @@ -315,10 +322,10 @@ mod tests { missing["metadata"] .as_object_mut() .unwrap() - .remove("system_input"); + .remove("input_metadata"); assert!(decode(missing)); let mut absent_policy = wire.clone(); - absent_policy["metadata"]["system_input"] + absent_policy["metadata"]["input_metadata"] .as_object_mut() .unwrap() .remove("usage"); @@ -327,15 +334,18 @@ mod tests { invalid_role["item"] = serde_json::to_value(LoggedItem::from(agen::Item::user_message("not system"))) .unwrap(); + invalid_role["metadata"]["input_metadata"]["public"] = serde_json::json!({ + "kind": "context", "body": "public system projection", "presentation": {"kind": "context"} + }); assert!(decode(invalid_role)); let mut oversized = wire.clone(); if generic { oversized["item"]["body"] = "x" - .repeat(crate::system_input::MAX_SYSTEM_INPUT_BYTES + 1) + .repeat(crate::input_metadata::MAX_SYSTEM_INPUT_BYTES + 1) .into(); } else { oversized["item"]["content"][0]["text"] = "x" - .repeat(crate::system_input::MAX_SYSTEM_INPUT_BYTES + 1) + .repeat(crate::input_metadata::MAX_SYSTEM_INPUT_BYTES + 1) .into(); } assert!(decode(oversized)); diff --git a/crates/session-store/src/system_input.rs b/crates/session-store/src/input_metadata.rs similarity index 82% rename from crates/session-store/src/system_input.rs rename to crates/session-store/src/input_metadata.rs index 3595c7bdb..2d29d07da 100644 --- a/crates/session-store/src/system_input.rs +++ b/crates/session-store/src/input_metadata.rs @@ -1,4 +1,4 @@ -//! Validated, Feature-neutral system-input data. +//! Validated, Feature-neutral history-input policy and System extensions. //! //! The enclosing history annotation owns identity, origin and derivation. None of //! those descriptive facts selects a use policy here. Model text, independent use @@ -23,22 +23,22 @@ const MAX_JSON_DEPTH: usize = 16; const MAX_JSON_NODES: usize = 8192; #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] -pub enum SystemInputError { - #[error("system input payload schema is invalid")] +pub enum InputMetadataError { + #[error("history input payload schema is invalid")] InvalidSchema, - #[error("system input payload version must be nonzero")] + #[error("history input payload version must be nonzero")] InvalidVersion, #[error("system input resource limit exceeded")] ResourceLimit, - #[error("system input payload could not be serialized")] + #[error("history input payload could not be serialized")] Serialization, - #[error("system input payload schema or version is unsupported by this consumer")] + #[error("history input payload schema or version is unsupported by this consumer")] UnsupportedPayload, - #[error("system input payload does not match the owner's typed contract")] + #[error("history input payload does not match the owner's typed contract")] InvalidTypedPayload, - #[error("generic system input is missing explicit metadata")] + #[error("provided input is missing explicit metadata")] MissingMetadata, - #[error("system input metadata does not match its logical record")] + #[error("input metadata does not match its logical record")] InconsistentRecord, } @@ -70,7 +70,7 @@ struct PayloadWire { } impl TryFrom for FeatureInputPayload { - type Error = SystemInputError; + type Error = InputMetadataError; fn try_from(wire: PayloadWire) -> Result { if wire.schema.is_empty() @@ -80,10 +80,10 @@ impl TryFrom for FeatureInputPayload { byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"-_.:/".contains(&byte) }) { - return Err(SystemInputError::InvalidSchema); + return Err(InputMetadataError::InvalidSchema); } if wire.version == 0 { - return Err(SystemInputError::InvalidVersion); + return Err(InputMetadataError::InvalidVersion); } validate_json(&wire.value, MAX_FEATURE_PAYLOAD_BYTES)?; Ok(Self { @@ -99,11 +99,11 @@ impl FeatureInputPayload { schema: impl Into, version: u32, value: &T, - ) -> Result { + ) -> Result { PayloadWire { schema: schema.into(), version, - value: serde_json::to_value(value).map_err(|_| SystemInputError::Serialization)?, + value: serde_json::to_value(value).map_err(|_| InputMetadataError::Serialization)?, } .try_into() } @@ -122,12 +122,12 @@ impl FeatureInputPayload { &self, schema: &str, version: u32, - ) -> Result { + ) -> Result { if self.schema != schema || self.version != version { - return Err(SystemInputError::UnsupportedPayload); + return Err(InputMetadataError::UnsupportedPayload); } serde_json::from_value(self.value.clone()) - .map_err(|_| SystemInputError::InvalidTypedPayload) + .map_err(|_| InputMetadataError::InvalidTypedPayload) } } @@ -161,17 +161,20 @@ pub enum InputCompactionUse { /// Public presentation is deliberately not one of these use decisions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct SystemInputUsePolicy { +pub struct HistoryInputUsePolicy { pub recording_material: InputEvidenceUse, pub memory_evidence: InputEvidenceUse, pub observation: InputObservationUse, pub compaction: InputCompactionUse, } +/// Use policy is independent of logical role. Non-System derived Tool context +/// carries policy only; private Feature payload and public presentation belong +/// to the System envelope and are rejected on other roles at the history boundary. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(try_from = "MetadataWire")] -pub struct SystemInputMetadata { - usage: SystemInputUsePolicy, +pub struct HistoryInputMetadata { + usage: HistoryInputUsePolicy, feature_payload: Option, public: Option, } @@ -179,13 +182,13 @@ pub struct SystemInputMetadata { #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct MetadataWire { - usage: SystemInputUsePolicy, + usage: HistoryInputUsePolicy, feature_payload: Option, public: Option, } -impl TryFrom for SystemInputMetadata { - type Error = SystemInputError; +impl TryFrom for HistoryInputMetadata { + type Error = InputMetadataError; fn try_from(wire: MetadataWire) -> Result { if let Some(public) = &wire.public { @@ -194,10 +197,10 @@ impl TryFrom for SystemInputMetadata { || public.kind.len() > MAX_SCHEMA_BYTES || public.kind.chars().any(char::is_control) { - return Err(SystemInputError::InvalidSchema); + return Err(InputMetadataError::InvalidSchema); } let value = - serde_json::to_value(public).map_err(|_| SystemInputError::Serialization)?; + serde_json::to_value(public).map_err(|_| InputMetadataError::Serialization)?; validate_json(&value, MAX_SYSTEM_PUBLIC_BYTES)?; } Ok(Self { @@ -208,12 +211,12 @@ impl TryFrom for SystemInputMetadata { } } -impl SystemInputMetadata { +impl HistoryInputMetadata { pub fn new( - usage: SystemInputUsePolicy, + usage: HistoryInputUsePolicy, feature_payload: Option, public: Option, - ) -> Result { + ) -> Result { MetadataWire { usage, feature_payload, @@ -222,7 +225,7 @@ impl SystemInputMetadata { .try_into() } - pub fn usage(&self) -> SystemInputUsePolicy { + pub fn usage(&self) -> HistoryInputUsePolicy { self.usage } @@ -258,22 +261,22 @@ impl SystemInputMetadata { #[serde(try_from = "InputWire")] pub struct SystemInputEnvelope { body: String, - metadata: SystemInputMetadata, + metadata: HistoryInputMetadata, } #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct InputWire { body: String, - metadata: SystemInputMetadata, + metadata: HistoryInputMetadata, } impl TryFrom for SystemInputEnvelope { - type Error = SystemInputError; + type Error = InputMetadataError; fn try_from(wire: InputWire) -> Result { if wire.body.len() > MAX_SYSTEM_INPUT_BYTES { - return Err(SystemInputError::ResourceLimit); + return Err(InputMetadataError::ResourceLimit); } Ok(Self { body: wire.body, @@ -285,8 +288,8 @@ impl TryFrom for SystemInputEnvelope { impl SystemInputEnvelope { pub fn new( body: impl Into, - metadata: SystemInputMetadata, - ) -> Result { + metadata: HistoryInputMetadata, + ) -> Result { InputWire { body: body.into(), metadata, @@ -298,7 +301,7 @@ impl SystemInputEnvelope { &self.body } - pub fn metadata(&self) -> &SystemInputMetadata { + pub fn metadata(&self) -> &HistoryInputMetadata { &self.metadata } @@ -306,15 +309,15 @@ impl SystemInputEnvelope { Item::system_message(self.body.clone()) } - pub fn into_parts(self) -> (String, SystemInputMetadata) { + pub fn into_parts(self) -> (String, HistoryInputMetadata) { (self.body, self.metadata) } } -fn validate_json(value: &Value, max_bytes: usize) -> Result<(), SystemInputError> { - fn walk(value: &Value, depth: usize, remaining: &mut usize) -> Result<(), SystemInputError> { +fn validate_json(value: &Value, max_bytes: usize) -> Result<(), InputMetadataError> { + fn walk(value: &Value, depth: usize, remaining: &mut usize) -> Result<(), InputMetadataError> { if depth > MAX_JSON_DEPTH || *remaining == 0 { - return Err(SystemInputError::ResourceLimit); + return Err(InputMetadataError::ResourceLimit); } *remaining -= 1; match value { @@ -334,9 +337,9 @@ fn validate_json(value: &Value, max_bytes: usize) -> Result<(), SystemInputError } let mut remaining = MAX_JSON_NODES; walk(value, 0, &mut remaining)?; - let bytes = serde_json::to_vec(value).map_err(|_| SystemInputError::Serialization)?; + let bytes = serde_json::to_vec(value).map_err(|_| InputMetadataError::Serialization)?; if bytes.len() > max_bytes { - return Err(SystemInputError::ResourceLimit); + return Err(InputMetadataError::ResourceLimit); } Ok(()) } @@ -353,8 +356,8 @@ mod tests { omitted: bool, } - fn reference_policy() -> SystemInputUsePolicy { - SystemInputUsePolicy { + fn reference_policy() -> HistoryInputUsePolicy { + HistoryInputUsePolicy { recording_material: InputEvidenceUse::Exclude, memory_evidence: InputEvidenceUse::Exclude, observation: InputObservationUse::Hidden, @@ -375,7 +378,7 @@ mod tests { .unwrap(); let envelope = SystemInputEnvelope::new( "private model context", - SystemInputMetadata::new(reference_policy(), Some(payload), None).unwrap(), + HistoryInputMetadata::new(reference_policy(), Some(payload), None).unwrap(), ) .unwrap(); let restored: SystemInputEnvelope = @@ -387,7 +390,7 @@ mod tests { assert_eq!(saved_payload.version(), 99); assert_eq!( saved_payload.decode::("known-owner:context", 1), - Err(SystemInputError::UnsupportedPayload) + Err(InputMetadataError::UnsupportedPayload) ); assert_eq!( serde_json::to_value(&restored).unwrap(), @@ -415,7 +418,7 @@ mod tests { .unwrap(); assert_eq!( invalid.decode::("owner:context", 1), - Err(SystemInputError::InvalidTypedPayload) + Err(InputMetadataError::InvalidTypedPayload) ); } @@ -462,14 +465,14 @@ mod tests { ] { let mut absent = valid.clone(); absent.as_object_mut().unwrap().remove(field); - assert!(serde_json::from_value::(absent).is_err()); + assert!(serde_json::from_value::(absent).is_err()); let mut unknown = valid.clone(); unknown[field] = Value::String("future-allow".into()); - assert!(serde_json::from_value::(unknown).is_err()); + assert!(serde_json::from_value::(unknown).is_err()); } let mut extra = valid; extra["feature_name"] = Value::String("infer-policy-from-me".into()); - assert!(serde_json::from_value::(extra).is_err()); + assert!(serde_json::from_value::(extra).is_err()); } #[test] @@ -483,7 +486,7 @@ mod tests { body: "explicit public text".into(), presentation: PublicSystemPresentation::Context, }; - let metadata = SystemInputMetadata::new( + let metadata = HistoryInputMetadata::new( reference_policy(), Some( FeatureInputPayload::encode( @@ -519,14 +522,14 @@ mod tests { #[test] fn use_policies_remain_independent_of_each_other_and_of_public_display() { - let usage = SystemInputUsePolicy { + let usage = HistoryInputUsePolicy { recording_material: InputEvidenceUse::Include, memory_evidence: InputEvidenceUse::Exclude, observation: InputObservationUse::Readable, compaction: InputCompactionUse::Retain, }; - let metadata = SystemInputMetadata::new(usage, None, None).unwrap(); - let restored: SystemInputMetadata = + let metadata = HistoryInputMetadata::new(usage, None, None).unwrap(); + let restored: HistoryInputMetadata = serde_json::from_value(serde_json::to_value(&metadata).unwrap()).unwrap(); assert_eq!(restored.usage(), usage); assert!(restored.public_item().is_none()); @@ -535,7 +538,7 @@ mod tests { #[test] fn model_body_and_public_projection_have_independent_resource_limits() { - let metadata = SystemInputMetadata::new(reference_policy(), None, None).unwrap(); + let metadata = HistoryInputMetadata::new(reference_policy(), None, None).unwrap(); assert!( SystemInputEnvelope::new("x".repeat(MAX_SYSTEM_INPUT_BYTES + 1), metadata).is_err() ); @@ -544,7 +547,7 @@ mod tests { body: "x".repeat(MAX_SYSTEM_PUBLIC_BYTES), presentation: PublicSystemPresentation::Context, }; - assert!(SystemInputMetadata::new(reference_policy(), None, Some(public)).is_err()); + assert!(HistoryInputMetadata::new(reference_policy(), None, Some(public)).is_err()); let invalid = serde_json::json!({ "body": "small", "metadata": { "public": null, "feature_payload": null } }); assert!(serde_json::from_value::(invalid).is_err()); } diff --git a/crates/session-store/src/legacy_session_log.rs b/crates/session-store/src/legacy_session_log.rs index 10e247539..dc91caf07 100644 --- a/crates/session-store/src/legacy_session_log.rs +++ b/crates/session-store/src/legacy_session_log.rs @@ -1,4 +1,4 @@ -//! Versioned decoder for Session schemas v1-4, including frozen Feature wire. +//! Versioned decoder for Session schemas v1-5, including frozen Feature wire. //! //! These types are intentionally private to `session-store`. Current writers, //! replay, and public projections use [`crate::LogEntry`] exclusively; only the @@ -93,14 +93,14 @@ impl LegacySystemItem { fn into_entry( self, mut metadata: LoggedSessionHistoryMetadata, - ) -> Result { + ) -> Result { use crate::InputCompactionUse::{ReferenceInput, Retain}; let item = match self { Self::Core(item) => item, Self::Feature(feature) => { - metadata.validate_system_input()?; - if metadata.system_input.is_some() { - return Err(crate::SystemInputError::InconsistentRecord); + metadata.validate_input_metadata()?; + if metadata.input_metadata.is_some() { + return Err(crate::InputMetadataError::InconsistentRecord); } let (body, input_metadata) = match feature { LegacyFeatureSystemItem::JournalStartup { body, payload } => { @@ -150,12 +150,12 @@ impl LegacySystemItem { } }; // Identity/origin/derivation are retained, never re-inferred. - metadata.system_input = Some(input_metadata); + metadata.input_metadata = Some(input_metadata); SystemItem::Input { body } } }; let entry = LoggedSystemHistoryEntry { item, metadata }; - entry.validate_system_input()?; + entry.validate_input_metadata()?; Ok(entry) } } @@ -166,8 +166,8 @@ fn migrated_feature_input( schema: &str, payload: T, compaction: crate::InputCompactionUse, -) -> Result<(String, crate::SystemInputMetadata), crate::SystemInputError> { - let metadata = crate::SystemInputMetadata::new( +) -> Result<(String, crate::HistoryInputMetadata), crate::InputMetadataError> { + let metadata = crate::HistoryInputMetadata::new( migration_policy(compaction), Some(crate::FeatureInputPayload::encode(schema, 1, &payload)?), Some(crate::PublicSystemItem { @@ -207,7 +207,7 @@ enum LegacyAnnotatedOrCurrent { } impl LegacyAnnotatedOrCurrent { - fn into_current(self) -> Result { + fn into_current(self) -> Result { match self { Self::Annotated(LegacyAnnotatedLogEntry::AnnotatedSystemItem { ts, @@ -285,16 +285,19 @@ pub(crate) fn decode_entry( segment_id: SegmentId, line_index: usize, ) -> Result { + let mut wire: serde_json::Value = serde_json::from_str(line)?; + migrate_legacy_metadata_field(&mut wire)?; let entry = match schema_version { - 1 => match serde_json::from_str::(line)? { + 1 => match serde_json::from_value::(wire)? { LegacySessionLogEntryV1::History(entry) => Entry::History(entry), LegacySessionLogEntryV1::Current(entry) => Entry::Current(entry), }, - 2 => match serde_json::from_str::(line)? { + 2 => match serde_json::from_value::(wire)? { LegacySessionLogEntryV2::History(entry) => Entry::History(entry), LegacySessionLogEntryV2::Current(entry) => Entry::Current(entry), }, - 3 | 4 => Entry::Current(serde_json::from_str(line)?), + 3 | 4 => Entry::Current(serde_json::from_value(wire)?), + 5 => return serde_json::from_value(wire), _ => unreachable!("legacy decoder called for unsupported schema {schema_version}"), }; match entry { @@ -306,6 +309,53 @@ pub(crate) fn decode_entry( .map_err(serde::de::Error::custom) } +// The v5 System-only metadata field was widened to role-neutral input metadata +// in v6. Touch only known history slots, never arbitrary Feature payload objects. +// Both keys are an ambiguous source, not permission to discard either value. +fn migrate_legacy_metadata_field(record: &mut serde_json::Value) -> Result<(), serde_json::Error> { + fn entry(value: &mut serde_json::Value) -> Result<(), serde_json::Error> { + if let Some(metadata) = value + .get_mut("metadata") + .and_then(serde_json::Value::as_object_mut) + { + if let Some(old) = metadata.remove("system_input") { + if metadata.contains_key("input_metadata") { + return Err(serde::de::Error::custom( + "ambiguous legacy input metadata fields", + )); + } + metadata.insert("input_metadata".into(), old); + } + } + Ok(()) + } + let kind = record + .get("kind") + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + .to_owned(); + if matches!( + kind.as_str(), + "annotated_system_item" + | "annotated_assistant_item" + | "annotated_tool_result" + | "tool_result_corrected" + ) { + if let Some(value) = record.get_mut("entry") { + entry(value)?; + } + } + if let Some(history) = record + .get_mut("history") + .and_then(serde_json::Value::as_array_mut) + { + for value in history { + entry(value)?; + } + } + Ok(()) +} + /// Schema 3 used DerivedSummary for Host compaction seeds. Only the trusted /// creation boundary may identify one; arbitrary text and later seed items do not. /// Include already-upgraded entries so an interrupted multi-file migration resumes. @@ -361,15 +411,15 @@ pub(crate) fn migrate_compaction_origins( } } -/// Explicit v1-4 -> v5 boundary. No Feature inference belongs in ordinary +/// Explicit legacy-to-current boundary. No Feature inference belongs in ordinary /// readers/restoration. Invalid or oversized payloads fail migration; the generic /// payload/body/public limits are deliberately not relaxed for legacy data. -pub(crate) fn migrate_system_inputs(entry: &mut LogEntry) -> Result<(), crate::SystemInputError> { - entry.validate_system_inputs()?; +pub(crate) fn migrate_system_inputs(entry: &mut LogEntry) -> Result<(), crate::InputMetadataError> { + entry.validate_input_metadata()?; match entry { // Feature wire has already been converted by the explicit old-schema // decoder. Ordinary LogEntry can contain only generic Input/core events. - LogEntry::AnnotatedSystemItem { entry, .. } => entry.validate_system_input()?, + LogEntry::AnnotatedSystemItem { entry, .. } => entry.validate_input_metadata()?, LogEntry::AnnotatedSegmentStart { history, .. } | LogEntry::AnnotatedUserInput { history, .. } => { for entry in history { @@ -383,11 +433,11 @@ pub(crate) fn migrate_system_inputs(entry: &mut LogEntry) -> Result<(), crate::S } _ => {} } - entry.validate_system_inputs() + entry.validate_input_metadata() } -fn migration_policy(compaction: crate::InputCompactionUse) -> crate::SystemInputUsePolicy { - crate::SystemInputUsePolicy { +fn migration_policy(compaction: crate::InputCompactionUse) -> crate::HistoryInputUsePolicy { + crate::HistoryInputUsePolicy { recording_material: crate::InputEvidenceUse::Exclude, memory_evidence: crate::InputEvidenceUse::Exclude, observation: crate::InputObservationUse::Hidden, @@ -397,18 +447,22 @@ fn migration_policy(compaction: crate::InputCompactionUse) -> crate::SystemInput fn migrate_flattened_system_input( entry: &mut LoggedHistoryEntry, -) -> Result<(), crate::SystemInputError> { +) -> Result<(), crate::InputMetadataError> { // Check before supplying legacy policy: Provided without metadata is a // malformed NEW record, not permission to synthesize a producer policy. - entry.validate_system_input()?; - if entry.metadata.system_input.is_none() - && matches!( + entry.validate_input_metadata()?; + if entry.metadata.input_metadata.is_none() + && (matches!( entry.item, LoggedItem::Message { role: crate::LoggedRole::System, .. } - ) + ) || matches!( + entry.metadata.origin, + LoggedSessionHistoryOrigin::DerivedSummary + | LoggedSessionHistoryOrigin::CompactionSummary + )) { let compaction = match entry.metadata.origin { LoggedSessionHistoryOrigin::JournalContext => crate::InputCompactionUse::ReferenceInput, @@ -417,14 +471,16 @@ fn migrate_flattened_system_input( } _ => crate::InputCompactionUse::Retain, }; - // No Feature payload or public projection can be recovered from text. - entry.metadata.system_input = Some(crate::SystemInputMetadata::new( + // Legacy derived context was excluded from independent evidence. Preserve + // that established decision explicitly, including synthetic Tool roles; + // never guess from Tool names or body text. Payload/public stay absent. + entry.metadata.input_metadata = Some(crate::HistoryInputMetadata::new( migration_policy(compaction), None, None, )?); } - entry.validate_system_input() + entry.validate_input_metadata() } enum Entry { @@ -442,7 +498,7 @@ fn legacy_metadata( identity.extend_from_slice(&(line_index as u64).to_be_bytes()); identity.extend_from_slice(&(item_index as u64).to_be_bytes()); LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId(format!( "l-{}", base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, identity) @@ -457,7 +513,7 @@ fn canonicalize_history_entry( segment_id: SegmentId, line_index: usize, entry: LegacyHistoryLogEntry, -) -> Result { +) -> Result { Ok(match entry { LegacyHistoryLogEntry::SegmentStart { ts, diff --git a/crates/session-store/src/legacy_session_log/tests.rs b/crates/session-store/src/legacy_session_log/tests.rs index d7914c194..e879d0ab1 100644 --- a/crates/session-store/src/legacy_session_log/tests.rs +++ b/crates/session-store/src/legacy_session_log/tests.rs @@ -24,7 +24,7 @@ fn seed( content: vec![crate::LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId("summary".into()), origin, derivation: Some(crate::LoggedHistoryDerivation { diff --git a/crates/session-store/src/legacy_session_log/tests/fixtures/v5-role-metadata.jsonl b/crates/session-store/src/legacy_session_log/tests/fixtures/v5-role-metadata.jsonl new file mode 100644 index 000000000..533bf518d --- /dev/null +++ b/crates/session-store/src/legacy_session_log/tests/fixtures/v5-role-metadata.jsonl @@ -0,0 +1,5 @@ +{"kind":"annotated_system_item","ts":201,"entry":{"item":{"kind":"input","body":"frozen v5 reference"},"metadata":{"entry_id":"reference-v5","origin":{"kind":"provided","producer":"owner-v5"},"system_input":{"usage":{"recording_material":"exclude","memory_evidence":"exclude","observation":"hidden","compaction":"reference_input"},"feature_payload":{"schema":"future:owner","version":8,"value":{"system_input":"opaque owner field must not be renamed"}},"public":null}}}} +{"kind":"annotated_assistant_item","ts":202,"entry":{"item":{"kind":"tool_call","call_id":"original-call","name":"ArbitraryTool","arguments":"{}"},"metadata":{"entry_id":"original-call-entry","origin":{"kind":"model_output","worker":{"worker_id":"body"}}}}} +{"kind":"annotated_tool_result","ts":203,"entry":{"item":{"kind":"tool_result","call_id":"original-call","summary":"original result"},"metadata":{"entry_id":"original-result-entry","origin":{"kind":"tool_output","worker":{"worker_id":"body"}}}}} +{"kind":"annotated_assistant_item","ts":204,"entry":{"item":{"kind":"tool_call","call_id":"derived-call","name":"ArbitraryTool","arguments":"{}"},"metadata":{"entry_id":"derived-call-entry","origin":{"kind":"derived_summary"},"derivation":{"sources":["original-call-entry","original-result-entry"]}}}} +{"kind":"annotated_tool_result","ts":205,"entry":{"item":{"kind":"tool_result","call_id":"derived-call","summary":"derived retrieval result"},"metadata":{"entry_id":"derived-result-entry","origin":{"kind":"derived_summary"},"derivation":{"sources":["original-call-entry","original-result-entry"]}}}} diff --git a/crates/session-store/src/legacy_session_log/tests/v5.rs b/crates/session-store/src/legacy_session_log/tests/v5.rs index f197eeec9..847b81bf0 100644 --- a/crates/session-store/src/legacy_session_log/tests/v5.rs +++ b/crates/session-store/src/legacy_session_log/tests/v5.rs @@ -27,7 +27,7 @@ fn encode(entries: &[LogEntry]) -> Vec { fn migrate(entries: &mut [LogEntry]) { for entry in entries { migrate_system_inputs(entry).unwrap(); - entry.validate_system_inputs().unwrap(); + entry.validate_input_metadata().unwrap(); } } @@ -100,7 +100,7 @@ fn v4_fixed_wire_preserves_identity_body_payload_public_and_independent_policy() assert_eq!(entry.metadata.entry_id, old_metadata.entry_id); assert_eq!(entry.metadata.origin, old_metadata.origin); assert_eq!(entry.metadata.derivation, old_metadata.derivation); - let metadata = entry.metadata.system_input.as_ref().unwrap(); + let metadata = entry.metadata.input_metadata.as_ref().unwrap(); let public = metadata.public_item().unwrap(); assert_eq!(public.kind, old_item["kind"].as_str().unwrap()); assert_eq!(public.body, old_item["body"].as_str().unwrap()); @@ -182,7 +182,7 @@ fn v4_fixed_wire_preserves_identity_body_payload_public_and_independent_policy() history[index].metadata.derivation, old[index].metadata.derivation ); - let metadata = history[index].metadata.system_input.as_ref().unwrap(); + let metadata = history[index].metadata.input_metadata.as_ref().unwrap(); assert_eq!(metadata.usage(), migration_policy(compaction)); assert!(metadata.public_item().is_none()); assert!(metadata.feature_payload().is_none()); @@ -225,7 +225,7 @@ fn v1_through_v4_worker_migration_reopen_and_mixed_file_retry() { fs::write(&old_path, source).unwrap(); let store = WorkerSessionStore::new(root.path()).unwrap(); - assert_marker(root.path(), 5); + assert_marker(root.path(), 6); let restored = store.read_all(session, segment).unwrap(); assert_eq!(encode(&restored), new_bytes); assert_eq!(fs::read(&new_path).unwrap(), new_bytes); @@ -243,7 +243,7 @@ fn v1_through_v4_worker_migration_reopen_and_mixed_file_retry() { fn all_segments_preflight_keeps_old_bytes_on_decode_or_conversion_failure() { let mut oversized: Value = serde_json::from_str(V4.lines().next().unwrap()).unwrap(); oversized["entry"]["item"]["snapshot"] = - json!({"large": "x".repeat(crate::system_input::MAX_FEATURE_PAYLOAD_BYTES + 1)}); + json!({"large": "x".repeat(crate::input_metadata::MAX_FEATURE_PAYLOAD_BYTES + 1)}); let mut unknown: Value = serde_json::from_str(V4.lines().next().unwrap()).unwrap(); unknown["entry"]["item"]["kind"] = json!("unsupported_future_feature"); let mut malformed = unknown.clone(); @@ -295,7 +295,7 @@ fn all_segments_preflight_keeps_old_bytes_on_decode_or_conversion_failure() { #[test] fn unsupported_schema_preserves_all_old_bytes() { - for version in [0, 6, 99] { + for version in [0, 7, 99] { let root = tempfile::tempdir().unwrap(); let (_, segment) = write_worker(root.path(), version, V4.as_bytes()); let path = root @@ -331,13 +331,13 @@ fn standalone_store_uses_same_v1_through_v4_migration_and_reopens() { let store = FsStore::new(root.path()).unwrap(); let entries = store.read_all(session, segment).unwrap(); for entry in entries { - entry.validate_system_inputs().unwrap(); + entry.validate_input_metadata().unwrap(); } assert_eq!( fs::read_to_string(dir.join("session-schema.json")) .unwrap() .trim(), - "5" + "6" ); let once = fs::read(&path).unwrap(); drop(store); @@ -381,7 +381,7 @@ fn compaction_origin_upgrade_is_retained_only_for_pre_v4_sources() { } ); assert_eq!( - metadata.system_input.as_ref().unwrap().usage().compaction, + metadata.input_metadata.as_ref().unwrap().usage().compaction, if version == 3 { InputCompactionUse::OpeningSummary } else { @@ -423,7 +423,7 @@ fn current_v5_rejects_all_legacy_feature_kinds_without_rewriting_sources() { assert!(serde_json::from_str::(line).is_err()); let bytes = format!("{line}\n"); let root = tempfile::tempdir().unwrap(); - let (session, segment) = write_worker(root.path(), 5, bytes.as_bytes()); + let (session, segment) = write_worker(root.path(), 6, bytes.as_bytes()); let path = root .path() .join("segments") @@ -495,7 +495,7 @@ fn v1_v2_feature_wire_uses_the_same_typed_payloads_and_stable_legacy_identity() else { panic!() }; - assert_eq!(entry.metadata.system_input, v4.metadata.system_input); + assert_eq!(entry.metadata.input_metadata, v4.metadata.input_metadata); } } } @@ -555,7 +555,7 @@ fn frozen_behavior_counter_is_accepted_only_at_the_old_schema_boundary() { assert_eq!(entry.item.history_text(), "exact archived body"); let payload = entry .metadata - .system_input + .input_metadata .as_ref() .unwrap() .feature_payload() @@ -573,7 +573,7 @@ fn frozen_behavior_counter_is_accepted_only_at_the_old_schema_boundary() { } #[test] -fn current_v5_rejects_generic_input_without_metadata_instead_of_migrating_it() { +fn current_schema_rejects_generic_input_without_metadata_instead_of_migrating_it() { let mut missing: Value = serde_json::from_str(V4.lines().last().unwrap()).unwrap(); missing["entry"]["metadata"] .as_object_mut() @@ -581,7 +581,7 @@ fn current_v5_rejects_generic_input_without_metadata_instead_of_migrating_it() { .remove("system_input"); let bytes = format!("{}\n", missing); let root = tempfile::tempdir().unwrap(); - let (_, segment) = write_worker(root.path(), 5, bytes.as_bytes()); + let (_, segment) = write_worker(root.path(), 6, bytes.as_bytes()); assert!(WorkerSessionStore::new(root.path()).is_err()); assert_eq!( fs::read( @@ -592,5 +592,112 @@ fn current_v5_rejects_generic_input_without_metadata_instead_of_migrating_it() { .unwrap(), bytes.as_bytes() ); - assert_marker(root.path(), 5); + assert_marker(root.path(), 6); +} + +const V5: &str = include_str!("fixtures/v5-role-metadata.jsonl"); + +#[test] +fn v5_metadata_and_derived_tool_context_upgrade_without_changing_roles_or_original_evidence() { + for standalone in [false, true] { + let root = tempfile::tempdir().unwrap(); + let (session, segment) = if standalone { + let session = SessionId::now_v7(); + let segment = SegmentId::now_v7(); + let dir = root.path().join(session.to_string()); + fs::create_dir_all(&dir).unwrap(); + fs::write(dir.join("session-schema.json"), b"5").unwrap(); + fs::write(dir.join(format!("{segment}.jsonl")), V5).unwrap(); + (session, segment) + } else { + write_worker(root.path(), 5, V5.as_bytes()) + }; + let entries = if standalone { + FsStore::new(root.path()) + .unwrap() + .read_all(session, segment) + .unwrap() + } else { + WorkerSessionStore::new(root.path()) + .unwrap() + .read_all(session, segment) + .unwrap() + }; + let originals: Vec = V5 + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect(); + assert_eq!(entries.len(), originals.len()); + for (index, (entry, old)) in entries.iter().zip(originals).enumerate() { + let wire = serde_json::to_value(entry).unwrap(); + assert_eq!(wire["ts"], old["ts"]); + assert_eq!(wire["entry"]["item"], old["entry"]["item"]); + for field in ["entry_id", "origin", "derivation"] { + assert_eq!( + wire["entry"]["metadata"][field], + old["entry"]["metadata"][field] + ); + } + let metadata = &wire["entry"]["metadata"]; + if index == 0 { + assert_eq!( + metadata["input_metadata"], + old["entry"]["metadata"]["system_input"] + ); + assert_eq!( + metadata["input_metadata"]["feature_payload"]["value"]["system_input"], + "opaque owner field must not be renamed" + ); + } else if index < 3 { + assert!( + metadata.get("input_metadata").is_none(), + "ordinary model Tool originals must not acquire exclusion policy" + ); + } else { + assert_eq!( + metadata["input_metadata"]["usage"], + json!({ + "recording_material":"exclude", "memory_evidence":"exclude", "observation":"hidden", "compaction":"retain" + }) + ); + } + } + let before = encode(&entries); + let reopened = if standalone { + FsStore::new(root.path()) + .unwrap() + .read_all(session, segment) + .unwrap() + } else { + WorkerSessionStore::new(root.path()) + .unwrap() + .read_all(session, segment) + .unwrap() + }; + assert_eq!(encode(&reopened), before); + } +} + +#[test] +fn legacy_metadata_alias_conflict_fails_preflight_without_dropping_either_value() { + let mut bad: Value = serde_json::from_str(V5.lines().next().unwrap()).unwrap(); + bad["entry"]["metadata"]["input_metadata"] = bad["entry"]["metadata"]["system_input"].clone(); + for version in [4, 5] { + let root = tempfile::tempdir().unwrap(); + let (_, segment) = write_worker(root.path(), version, V5.as_bytes()); + let path = root + .path() + .join("segments") + .join(format!("{segment}.jsonl")); + let later = root + .path() + .join("segments") + .join(format!("{}.jsonl", SegmentId::from_u128(2))); + let bad_bytes = format!("{bad}\n"); + fs::write(&later, &bad_bytes).unwrap(); + assert!(WorkerSessionStore::new(root.path()).is_err()); + assert_eq!(fs::read(path).unwrap(), V5.as_bytes()); + assert_eq!(fs::read(later).unwrap(), bad_bytes.as_bytes()); + assert_marker(root.path(), version); + } } diff --git a/crates/session-store/src/lib.rs b/crates/session-store/src/lib.rs index 7750f87e0..d767defca 100644 --- a/crates/session-store/src/lib.rs +++ b/crates/session-store/src/lib.rs @@ -41,6 +41,7 @@ pub(crate) fn read_without_atime_bounded(path: &Path, max_bytes: u64) -> io::Res pub mod event_trace; pub mod fs_store; pub mod history; +pub mod input_metadata; mod legacy_session_log; pub mod logged_item; mod paste_artifact; @@ -51,7 +52,6 @@ pub mod retention; pub mod segment; pub mod segment_log; pub mod store; -pub mod system_input; pub mod system_item; pub mod uploaded_file; pub mod worker_metadata; @@ -66,6 +66,10 @@ pub use history::{ LoggedSessionHistoryMetadata, LoggedSessionHistoryOrigin, LoggedSystemHistoryEntry, LoggedWorkerSubject, }; +pub use input_metadata::{ + FeatureInputPayload, HistoryInputMetadata, HistoryInputUsePolicy, InputCompactionUse, + InputEvidenceUse, InputMetadataError, InputObservationUse, SystemInputEnvelope, +}; pub use logged_item::{LoggedContentPart, LoggedItem, LoggedRole, from_logged, to_logged}; pub use paste_artifact::PasteArtifactLimits; pub use public_index::{ @@ -98,10 +102,6 @@ pub use segment::{ }; pub use segment_log::{LogEntry, RestoredState, SegmentOrigin, SessionExtension, collect_state}; pub use store::{Store, StoreError}; -pub use system_input::{ - FeatureInputPayload, InputCompactionUse, InputEvidenceUse, InputObservationUse, - SystemInputEnvelope, SystemInputError, SystemInputMetadata, SystemInputUsePolicy, -}; pub use system_item::{ PromptRenderProvenance, PublicSystemItem, PublicSystemPresentation, SystemItem, SystemReminder, SystemReminderSource, render_worker_event, diff --git a/crates/session-store/src/public_index.rs b/crates/session-store/src/public_index.rs index cba29bc87..eff4c04ad 100644 --- a/crates/session-store/src/public_index.rs +++ b/crates/session-store/src/public_index.rs @@ -1296,7 +1296,7 @@ mod tests { LoggedHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId(id.to_owned()), origin, derivation: None, @@ -1538,7 +1538,7 @@ mod tests { prompt_provenance: None, }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId("system-item".into()), origin: LoggedSessionHistoryOrigin::BackendInstruction { operation_id: None, diff --git a/crates/session-store/src/public_material.rs b/crates/session-store/src/public_material.rs index 2c7d66293..a7cdccaf2 100644 --- a/crates/session-store/src/public_material.rs +++ b/crates/session-store/src/public_material.rs @@ -371,11 +371,10 @@ impl SessionPublicMaterialReader { let source_ids: Option> = if compact { let summaries: Vec<_> = history .iter() - .filter(|e| { - matches!( - e.metadata.origin, - LoggedSessionHistoryOrigin::CompactionSummary - ) + .filter(|entry| { + entry.metadata.input_metadata.as_ref().is_some_and(|input| { + input.usage().compaction == crate::InputCompactionUse::OpeningSummary + }) }) .collect(); if summaries.len() != 1 { @@ -895,7 +894,7 @@ fn canonical_body( entry: &LoggedHistoryEntry, purpose: PublicMaterialUse, ) -> Option> { - if let Some(input) = &entry.metadata.system_input { + if let Some(input) = &entry.metadata.input_metadata { let usage = input.usage(); let allowed = match purpose { PublicMaterialUse::Recording => usage.recording_material, diff --git a/crates/session-store/src/public_material/tests.rs b/crates/session-store/src/public_material/tests.rs index 5bf7ce7ea..230b81055 100644 --- a/crates/session-store/src/public_material/tests.rs +++ b/crates/session-store/src/public_material/tests.rs @@ -9,7 +9,7 @@ fn entry(id: &str, item: LoggedItem) -> LoggedHistoryEntry { LoggedHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId(id.into()), origin: LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, @@ -31,9 +31,9 @@ fn system_context( compaction: crate::InputCompactionUse, ) -> LoggedHistoryEntry { let mut entry = message(id, LoggedRole::System, text); - entry.metadata.system_input = Some( - crate::SystemInputMetadata::new( - crate::SystemInputUsePolicy { + entry.metadata.input_metadata = Some( + crate::HistoryInputMetadata::new( + crate::HistoryInputUsePolicy { recording_material: crate::InputEvidenceUse::Exclude, memory_evidence: crate::InputEvidenceUse::Exclude, observation: crate::InputObservationUse::Hidden, @@ -613,9 +613,9 @@ fn reasoning_proves_rollback_safety_but_private_and_derived_items_are_not_materi body: "previous Journal must not become new recording material".into(), }, metadata: LoggedSessionHistoryMetadata { - system_input: Some( - crate::SystemInputMetadata::new( - crate::SystemInputUsePolicy { + input_metadata: Some( + crate::HistoryInputMetadata::new( + crate::HistoryInputUsePolicy { recording_material: crate::InputEvidenceUse::Exclude, memory_evidence: crate::InputEvidenceUse::Exclude, observation: crate::InputObservationUse::Hidden, @@ -812,9 +812,9 @@ fn schema_v3_compaction_migrates_persistently_and_strict_reader_keeps_original_i panic!() }; summary.metadata.origin = LoggedSessionHistoryOrigin::CompactionSummary; - summary.metadata.system_input = Some( - crate::SystemInputMetadata::new( - crate::SystemInputUsePolicy { + summary.metadata.input_metadata = Some( + crate::HistoryInputMetadata::new( + crate::HistoryInputUsePolicy { recording_material: crate::InputEvidenceUse::Exclude, memory_evidence: crate::InputEvidenceUse::Exclude, observation: crate::InputObservationUse::Hidden, @@ -878,7 +878,9 @@ fn midturn_compaction_uses_exact_source_ids_and_preserves_discarded_originals() "never use as original", crate::InputCompactionUse::OpeningSummary, ); - summary.metadata.origin = LoggedSessionHistoryOrigin::CompactionSummary; + summary.metadata.origin = LoggedSessionHistoryOrigin::Provided { + producer: "renamed-summary-owner".into(), + }; summary.metadata.derivation = Some(crate::LoggedHistoryDerivation { sources: vec![ LoggedSessionHistoryEntryId("u".into()), @@ -1311,8 +1313,8 @@ fn compaction_without_exact_derivation_or_with_invalid_turn_is_rejected() { #[test] fn generic_system_material_uses_explicit_evidence_purpose_not_producer_or_body() { use crate::{ - FeatureInputPayload, InputCompactionUse, InputEvidenceUse, InputObservationUse, - LoggedSystemHistoryEntry, SystemInputEnvelope, SystemInputMetadata, SystemInputUsePolicy, + FeatureInputPayload, HistoryInputMetadata, HistoryInputUsePolicy, InputCompactionUse, + InputEvidenceUse, InputObservationUse, LoggedSystemHistoryEntry, SystemInputEnvelope, }; for worker_layout in [false, true] { let temp = tempfile::tempdir().unwrap(); @@ -1328,8 +1330,8 @@ fn generic_system_material_uses_explicit_evidence_purpose_not_producer_or_body() None, SystemInputEnvelope::new( format!("private model text: {body}"), - SystemInputMetadata::new( - SystemInputUsePolicy { + HistoryInputMetadata::new( + HistoryInputUsePolicy { recording_material, memory_evidence, observation: InputObservationUse::Hidden, diff --git a/crates/session-store/src/public_snapshot.rs b/crates/session-store/src/public_snapshot.rs index 1baa121c8..ce23ee221 100644 --- a/crates/session-store/src/public_snapshot.rs +++ b/crates/session-store/src/public_snapshot.rs @@ -1625,9 +1625,9 @@ pub(crate) fn project_session_snapshot_for_segment( derivation_ids(entry), entry .metadata - .system_input + .input_metadata .as_ref() - .map(crate::SystemInputMetadata::public_use_policy), + .map(crate::HistoryInputMetadata::public_use_policy), )), LogEntry::RunYielded { ts, @@ -1797,9 +1797,9 @@ fn history_entry( SessionSnapshotEntry { use_policy: entry .metadata - .system_input + .input_metadata .as_ref() - .map(crate::SystemInputMetadata::public_use_policy), + .map(crate::HistoryInputMetadata::public_use_policy), entry_id: entry.metadata.entry_id.0.clone(), timestamp, provenance: provenance(&entry.metadata.origin), @@ -1926,7 +1926,16 @@ pub fn session_tool_attachment_id( } fn project_history_item(entry: &LoggedHistoryEntry) -> Option { - if let Some(input) = &entry.metadata.system_input { + if let Some(input) = &entry.metadata.input_metadata { + if !matches!( + entry.item, + LoggedItem::Message { + role: LoggedRole::System, + .. + } + ) { + return None; + } let public = input.public_projection(); return Some(SessionSnapshotEntryData::SystemItem { item_kind: public.kind.clone(), @@ -2310,7 +2319,7 @@ mod tests { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId(entry_id.into()), origin, derivation: None, @@ -3609,7 +3618,7 @@ mod tests { .map(|item| LoggedHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -3664,7 +3673,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: user_entry_id, origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".into(), @@ -3734,7 +3743,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::FlowInstruction { selector: "builtin:coder-review".into(), @@ -3754,7 +3763,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: user_entry_id.clone(), origin: origin.clone(), derivation: Some(LoggedHistoryDerivation { @@ -3785,7 +3794,7 @@ mod tests { fn annotated_projection_preserves_identity_and_provenance() { let session_id = crate::new_session_id(); let metadata = LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::ModelOutput { worker: LoggedWorkerSubject { @@ -4348,7 +4357,7 @@ mod tests { is_error: false, }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId("ancestor-image".into()), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -4443,7 +4452,7 @@ mod tests { is_error: false, }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId("tool-images".into()), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/session-store/src/segment_log.rs b/crates/session-store/src/segment_log.rs index f76ea783c..d54a2659d 100644 --- a/crates/session-store/src/segment_log.rs +++ b/crates/session-store/src/segment_log.rs @@ -255,18 +255,18 @@ pub struct SegmentOrigin { impl LogEntry { /// Validate generic System records before a Store publishes any bytes. - pub fn validate_system_inputs(&self) -> Result<(), crate::SystemInputError> { + pub fn validate_input_metadata(&self) -> Result<(), crate::InputMetadataError> { match self { Self::AnnotatedSegmentStart { history, .. } | Self::AnnotatedUserInput { history, .. } => { for entry in history { - entry.validate_system_input()?; + entry.validate_input_metadata()?; } } Self::AnnotatedAssistantItem { entry, .. } | Self::AnnotatedToolResult { entry, .. } - | Self::ToolResultCorrected { entry, .. } => entry.validate_system_input()?, - Self::AnnotatedSystemItem { entry, .. } => entry.validate_system_input()?, + | Self::ToolResultCorrected { entry, .. } => entry.validate_input_metadata()?, + Self::AnnotatedSystemItem { entry, .. } => entry.validate_input_metadata()?, _ => {} } Ok(()) @@ -554,7 +554,7 @@ mod tests { LoggedHistoryEntry { item: LoggedItem::from(item), metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/session-store/src/store.rs b/crates/session-store/src/store.rs index bf29ccb63..ef26359c7 100644 --- a/crates/session-store/src/store.rs +++ b/crates/session-store/src/store.rs @@ -27,8 +27,8 @@ pub enum StoreError { #[error("serialization error: {0}")] Serde(#[from] serde_json::Error), - #[error("invalid system input: {0}")] - InvalidSystemInput(#[from] crate::SystemInputError), + #[error("invalid history input metadata: {0}")] + InvalidInputMetadata(#[from] crate::InputMetadataError), #[error("segment not found: {0}")] NotFound(SegmentId), diff --git a/crates/session-store/src/worker_session_store.rs b/crates/session-store/src/worker_session_store.rs index 17a2691d1..9c6d8db00 100644 --- a/crates/session-store/src/worker_session_store.rs +++ b/crates/session-store/src/worker_session_store.rs @@ -36,7 +36,8 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; -const SESSION_SCHEMA_VERSION: u32 = 5; +const SESSION_SCHEMA_VERSION: u32 = 6; +const SYSTEM_INPUT_SESSION_SCHEMA_VERSION: u32 = 5; const COMPACTION_SESSION_SCHEMA_VERSION: u32 = 4; const ANNOTATED_SESSION_SCHEMA_VERSION: u32 = 3; const PREVIOUS_SESSION_SCHEMA_VERSION: u32 = 2; @@ -225,8 +226,9 @@ impl WorkerSessionStore { LEGACY_SESSION_SCHEMA_VERSION | PREVIOUS_SESSION_SCHEMA_VERSION | ANNOTATED_SESSION_SCHEMA_VERSION - | COMPACTION_SESSION_SCHEMA_VERSION => { - migrate_segment_logs_to_v5( + | COMPACTION_SESSION_SCHEMA_VERSION + | SYSTEM_INPUT_SESSION_SCHEMA_VERSION => { + migrate_segment_logs_to_current( segment_log_paths(&root)?, manifest.session_id, manifest.schema_version, @@ -753,7 +755,7 @@ impl Store for WorkerSessionStore { segment_id: SegmentId, entry: &LogEntry, ) -> Result<(), StoreError> { - entry.validate_system_inputs()?; + entry.validate_input_metadata()?; let _retention = self.lock_publication(session_id)?; self.ensure_session(session_id, true)?; self.append_log_entry(&self.log_path(segment_id), entry) @@ -813,7 +815,7 @@ impl Store for WorkerSessionStore { entries: &[LogEntry], ) -> Result<(), StoreError> { for entry in entries { - entry.validate_system_inputs()?; + entry.validate_input_metadata()?; } let _retention = self.lock_publication(session_id)?; self.ensure_session(session_id, true)?; @@ -1173,7 +1175,7 @@ fn segment_log_paths_in_directory( Ok(paths) } -fn migrate_segment_logs_to_v5( +fn migrate_segment_logs_to_current( paths: Vec<(SegmentId, PathBuf)>, session_id: SessionId, source_schema_version: u32, @@ -1220,7 +1222,7 @@ fn migrate_segment_logs_to_v5( crate::legacy_session_log::migrate_compaction_origins(&mut plan.entries, &summaries); for mut entry in std::mem::take(&mut plan.entries) { crate::legacy_session_log::migrate_system_inputs(&mut entry)?; - entry.validate_system_inputs()?; + entry.validate_input_metadata()?; serde_json::to_writer(&mut plan.output, &entry)?; plan.output.push(b'\n'); } @@ -1265,8 +1267,9 @@ pub(crate) fn migrate_fs_session(root: &Path, session_id: SessionId) -> Result<( LEGACY_SESSION_SCHEMA_VERSION | PREVIOUS_SESSION_SCHEMA_VERSION | ANNOTATED_SESSION_SCHEMA_VERSION - | COMPACTION_SESSION_SCHEMA_VERSION => { - migrate_segment_logs_to_v5( + | COMPACTION_SESSION_SCHEMA_VERSION + | SYSTEM_INPUT_SESSION_SCHEMA_VERSION => { + migrate_segment_logs_to_current( segment_log_paths_in_directory(root)?, session_id, version, @@ -1289,7 +1292,7 @@ fn validate_canonical_segment_paths(paths: Vec<(SegmentId, PathBuf)>) -> Result< for (_, path) in paths { let entries: Vec = parse_jsonl(&fs::read(&path)?)?; for entry in entries { - entry.validate_system_inputs()?; + entry.validate_input_metadata()?; } } Ok(()) @@ -1470,7 +1473,7 @@ mod tests { LoggedHistoryEntry { item: LoggedItem::from(item), metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -2162,7 +2165,7 @@ mod tests { assert_eq!(fs::read(&marker_path).unwrap(), old_marker); let updated_first: Vec = parse_jsonl(&first_published).unwrap(); for entry in &updated_first { - entry.validate_system_inputs().unwrap(); + entry.validate_input_metadata().unwrap(); } assert!(matches!( &updated_first[0], @@ -2184,7 +2187,7 @@ mod tests { assert_eq!(identity_and_body(&first), first_identity); assert_eq!(identity_and_body(&second), second_identity); for entry in first.iter().chain(second.iter()) { - entry.validate_system_inputs().unwrap(); + entry.validate_input_metadata().unwrap(); } assert_eq!(fs::read(&first_path).unwrap(), first_published); let second_published = fs::read(&second_path).unwrap(); diff --git a/crates/session-store/tests/fs_store_test.rs b/crates/session-store/tests/fs_store_test.rs index af4e940ec..9673980c8 100644 --- a/crates/session-store/tests/fs_store_test.rs +++ b/crates/session-store/tests/fs_store_test.rs @@ -11,7 +11,7 @@ fn annotated(item: Item) -> LoggedHistoryEntry { LoggedHistoryEntry { item: LoggedItem::from(item), metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -428,10 +428,9 @@ fn lookup_session_of_finds_owning_session() { #[test] fn generic_system_append_and_fork_preserve_metadata_without_public_payload_leak() { use session_store::{ - FeatureInputPayload, InputCompactionUse, InputEvidenceUse, InputObservationUse, - LoggedHistoryDerivation, LoggedSystemHistoryEntry, PublicSystemItem, - PublicSystemPresentation, SystemInputEnvelope, SystemInputMetadata, SystemInputUsePolicy, - WorkerSessionStore, + FeatureInputPayload, HistoryInputMetadata, HistoryInputUsePolicy, InputCompactionUse, + InputEvidenceUse, InputObservationUse, LoggedHistoryDerivation, LoggedSystemHistoryEntry, + PublicSystemItem, PublicSystemPresentation, SystemInputEnvelope, WorkerSessionStore, }; for worker_layout in [false, true] { let dir = tempfile::tempdir().unwrap(); @@ -455,8 +454,8 @@ fn generic_system_append_and_fork_preserve_metadata_without_public_payload_leak( }), SystemInputEnvelope::new( "private model body", - SystemInputMetadata::new( - SystemInputUsePolicy { + HistoryInputMetadata::new( + HistoryInputUsePolicy { recording_material: InputEvidenceUse::Exclude, memory_evidence: InputEvidenceUse::Exclude, observation: InputObservationUse::Hidden, @@ -584,11 +583,11 @@ fn invalid_generic_system_input_rejects_append_create_and_external_json_without_ }; assert!(matches!( store.append(sid, segment, &invalid), - Err(StoreError::InvalidSystemInput(_)) + Err(StoreError::InvalidInputMetadata(_)) )); assert!(matches!( store.create_segment(sid, segment, &[seed, invalid.clone()]), - Err(StoreError::InvalidSystemInput(_)) + Err(StoreError::InvalidInputMetadata(_)) )); assert!( serde_json::from_value::(serde_json::to_value(invalid).unwrap()).is_err() diff --git a/crates/session-store/tests/session_test.rs b/crates/session-store/tests/session_test.rs index e12d3ff29..dd6346be9 100644 --- a/crates/session-store/tests/session_test.rs +++ b/crates/session-store/tests/session_test.rs @@ -24,7 +24,7 @@ fn annotated(items: &[Item]) -> Vec { .map(|item| session_store::LoggedHistoryEntry { item: session_store::LoggedItem::from(item), metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId::new(), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/standalone/src/subjektiv/journal/tests.rs b/crates/standalone/src/subjektiv/journal/tests.rs index bd24ef67d..c488b4879 100644 --- a/crates/standalone/src/subjektiv/journal/tests.rs +++ b/crates/standalone/src/subjektiv/journal/tests.rs @@ -212,7 +212,7 @@ fn message(id: &str, text: &str) -> session_store::LoggedHistoryEntry { content: vec![session_store::LoggedContentPart::Text { text: text.into() }], }, metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId(id.into()), origin: session_store::LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, diff --git a/crates/standalone/src/subjektiv/journal/tests/compaction.rs b/crates/standalone/src/subjektiv/journal/tests/compaction.rs index b1aba547f..c9ebe8352 100644 --- a/crates/standalone/src/subjektiv/journal/tests/compaction.rs +++ b/crates/standalone/src/subjektiv/journal/tests/compaction.rs @@ -215,6 +215,7 @@ async fn public_mid_turn_compactions_preserve_originals_and_drain_after_cancel_a } protocol::Event::TextDelta { text } if text.contains(PARTIAL) => break, protocol::Event::RunEnd { .. } => panic!("dialog ended before cancellation"), + protocol::Event::SessionEntryCommitted { entry } if matches!(entry.data, protocol::SessionSnapshotEntryData::RunError { .. }) => panic!("dialog failed before cancellation: {entry:?}"), protocol::Event::Error { code, message } => panic!("{code:?}: {message}"), _ => {} } @@ -294,6 +295,32 @@ async fn public_mid_turn_compactions_preserve_originals_and_drain_after_cancel_a .iter() .flat_map(|segment| source_store.read_all(session, *segment).unwrap()) .collect(); + for history in raw.iter().filter_map(|record| match record { + session_store::LogEntry::AnnotatedSegmentStart { history, compacted_from: Some(_), .. } => Some(history), + _ => None, + }) { + let mut calls = std::collections::HashSet::new(); + let mut results = 0; + for entry in history { + let is_tool = matches!(entry.item, session_store::LoggedItem::ToolCall { .. } | session_store::LoggedItem::ToolResult { .. }); + let Some(metadata) = entry.metadata.input_metadata.as_ref().filter(|_| is_tool) else { continue; }; + assert_eq!(metadata.usage().recording_material, session_store::InputEvidenceUse::Exclude); + assert_eq!(metadata.usage().memory_evidence, session_store::InputEvidenceUse::Exclude); + assert_eq!(metadata.usage().observation, session_store::InputObservationUse::Hidden); + assert!(metadata.public_item().is_none()); + assert!(entry.metadata.derivation.is_some()); + match &entry.item { + session_store::LoggedItem::ToolCall { call_id, .. } => { calls.insert(call_id.clone()); } + session_store::LoggedItem::ToolResult { call_id, .. } => { + assert!(calls.contains(call_id), "derived Tool result must follow its real logical call"); + results += 1; + } + _ => unreachable!(), + } + } + assert!(!calls.is_empty()); + assert!(results >= calls.len()); + } assert_eq!( raw.iter() .filter(|entry| matches!( diff --git a/crates/standalone/src/subjektiv/journal/tests/connected_compaction.rs b/crates/standalone/src/subjektiv/journal/tests/connected_compaction.rs index 04032a8d7..c78ff4a66 100644 --- a/crates/standalone/src/subjektiv/journal/tests/connected_compaction.rs +++ b/crates/standalone/src/subjektiv/journal/tests/connected_compaction.rs @@ -11,7 +11,7 @@ use agen::llm_client::{ types::Item, }; use futures::Stream; -use session_store::{CombinedStore, FsWorkerStore, LogEntry, LoggedSessionHistoryOrigin}; +use session_store::{CombinedStore, FsWorkerStore, LogEntry}; use std::pin::Pin; const BEFORE: &str = "FROZEN_POSITION: B partially implemented; tests not run; result unconfirmed"; @@ -139,14 +139,14 @@ async fn local_host_compaction_freezes_pending_journal_through_request_and_durab let worker = prepared.worker(); assert_ne!(worker.segment_id(), source_segment); let seed = worker.store().read_all(worker.session_id(), worker.segment_id()).unwrap().into_iter().find_map(|entry| match entry { LogEntry::AnnotatedSegmentStart { history, .. } => Some(history), _ => None }).expect("durable compacted seed"); - let journal_items: Vec<_> = seed.iter().filter(|entry| entry.metadata.origin == LoggedSessionHistoryOrigin::JournalContext).collect(); + let journal_items: Vec<_> = seed.iter().filter(|entry| entry.metadata.input_metadata.as_ref().is_some_and(|input| input.usage().compaction == session_store::InputCompactionUse::ReferenceInput)).collect(); assert_eq!(journal_items.len(), 1); let journal_item = Item::from(journal_items[0].item.clone()); let body = journal_item.as_text().unwrap(); assert!(body.contains(&expected_json), "durable seed contains the full selected JSON and processing checkpoint"); assert!(body.contains(BEFORE)); assert!(!body.contains(AFTER)); - assert!(seed.iter().any(|entry| entry.metadata.origin == LoggedSessionHistoryOrigin::CompactionSummary && Item::from(entry.item.clone()).as_text().is_some_and(|text| text.contains(COMPLEMENT)))); + assert!(seed.iter().any(|entry| entry.metadata.input_metadata.as_ref().is_some_and(|input| input.usage().compaction == session_store::InputCompactionUse::OpeningSummary) && Item::from(entry.item.clone()).as_text().is_some_and(|text| text.contains(COMPLEMENT)))); let requests = model.requests.lock().unwrap(); let input = requests[0].items.iter().find_map(|item| match item { Item::Message { role: agen::llm_client::types::Role::User, .. } => item.as_text().filter(|text| text.contains("## Conversation overview/index")), diff --git a/crates/standalone/src/subjektiv/journal/tests/handoff.rs b/crates/standalone/src/subjektiv/journal/tests/handoff.rs index 03e5b6ebf..62d511c8f 100644 --- a/crates/standalone/src/subjektiv/journal/tests/handoff.rs +++ b/crates/standalone/src/subjektiv/journal/tests/handoff.rs @@ -79,7 +79,7 @@ async fn recorded_idle_body_handoff_preserves_open_after_source_deletion_and_see let sessions = session_store::FsStore::new_with_retention(workers.sessions_dir(second.worker_id()), crate::store::session_retention(&state)).unwrap(); let pointer = session_store::FsWorkerStore::new(workers.worker_metadata_dir(second.worker_id())).unwrap().read_by_name(&second.record().storage_key).unwrap().unwrap().active.unwrap(); let entries = sessions.read_all(session, pointer.segment_id.unwrap()).unwrap(); - let journal_items = entries.iter().filter(|entry| matches!(entry, session_store::LogEntry::AnnotatedSystemItem { entry, .. } if entry.metadata.system_input.as_ref().and_then(|input| input.feature_payload()).is_some_and(|payload| payload.schema() == "journal:startup"))).count(); + let journal_items = entries.iter().filter(|entry| matches!(entry, session_store::LogEntry::AnnotatedSystemItem { entry, .. } if entry.metadata.input_metadata.as_ref().and_then(|input| input.feature_payload()).is_some_and(|payload| payload.schema() == "journal:startup"))).count(); assert_eq!(journal_items, 1); assert_eq!(catalog.store.subject(&subject.id).unwrap().unwrap().behavior_md, "BEHAVIOR_ORIGINAL_UNCHANGED"); second.shutdown().await.unwrap(); diff --git a/crates/standalone/src/subjektiv_sessions.rs b/crates/standalone/src/subjektiv_sessions.rs index 71dc48631..9eee16adc 100644 --- a/crates/standalone/src/subjektiv_sessions.rs +++ b/crates/standalone/src/subjektiv_sessions.rs @@ -1621,7 +1621,7 @@ allow = [] let history = |id: &str, item: LoggedItem| LoggedHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId(id.into()), origin: LoggedSessionHistoryOrigin::BackendInstruction { operation_id: None }, derivation: None, diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index e537c5385..9a56b4c90 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -3567,7 +3567,7 @@ mod completion_flow_tests { session_store::LoggedHistoryEntry { item: session_store::LoggedItem::from(item), metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId::new(), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/worker-runtime/src/http_server/tests/session_material_tests.rs b/crates/worker-runtime/src/http_server/tests/session_material_tests.rs index 9c0e5f9e2..733f658ac 100644 --- a/crates/worker-runtime/src/http_server/tests/session_material_tests.rs +++ b/crates/worker-runtime/src/http_server/tests/session_material_tests.rs @@ -35,7 +35,7 @@ fn message(id: &str, role: LoggedRole, text: &str) -> LoggedHistoryEntry { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId(id.into()), origin: LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, diff --git a/crates/worker-runtime/src/retention.rs b/crates/worker-runtime/src/retention.rs index d488c9608..6e2496246 100644 --- a/crates/worker-runtime/src/retention.rs +++ b/crates/worker-runtime/src/retention.rs @@ -1867,7 +1867,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId("original".into()), origin: LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, @@ -1881,7 +1881,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId("summary".into()), origin: LoggedSessionHistoryOrigin::DerivedSummary, derivation: Some(session_store::LoggedHistoryDerivation { diff --git a/crates/worker-runtime/src/retention/runtime_v5_tests.rs b/crates/worker-runtime/src/retention/runtime_v5_tests.rs index 8c0123606..5527dba73 100644 --- a/crates/worker-runtime/src/retention/runtime_v5_tests.rs +++ b/crates/worker-runtime/src/retention/runtime_v5_tests.rs @@ -155,7 +155,7 @@ fn assert_generic(root: &Path, session: SessionId, segment: SegmentId) { entry.item.history_text(), old["entry"]["item"]["body"].as_str().unwrap() ); - let input = entry.metadata.system_input.as_ref().unwrap(); + let input = entry.metadata.input_metadata.as_ref().unwrap(); let usage = input.usage(); assert_eq!(usage.recording_material, InputEvidenceUse::Exclude); assert_eq!(usage.memory_evidence, InputEvidenceUse::Exclude); @@ -557,3 +557,78 @@ fn current_runtime_session_migration_still_rejects_live_worker_sockets() { assert_eq!(tree_bytes(&root), before); assert!(!temp.path().join(".runtime.schema-v12-staging").exists()); } + +#[test] +fn current_runtime_upgrades_v5_role_metadata_through_staging_without_reserializing_owner_evidence() +{ + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("runtime"); + let (worker, session, segment) = runtime_fixture(&root, 12); + let aggregate = root.join("workers").join(worker.to_string()); + let path = aggregate + .join("session/segments") + .join(format!("{segment}.jsonl")); + let old = fs::read_to_string(&path).unwrap(); + let header = old.lines().next().unwrap(); + let source = include_str!( + "../../../session-store/src/legacy_session_log/tests/fixtures/v5-role-metadata.jsonl" + ); + fs::write(&path, format!("{header}\n{source}")).unwrap(); + write_json( + &aggregate.join("session/session.json"), + &json!({"schema_version":5, "session_id": session}), + ); + let owner_before = fs::read(root.join("runtime.json")).unwrap(); + let worker_before = fs::read(aggregate.join("worker.json")).unwrap(); + let metadata_before = fs::read(aggregate.join("metadata.json")).unwrap(); + assert!(WorkerSessionStore::open_read_only(aggregate.join("session")).is_err()); + let options = FsRuntimeStoreOptions::new(&root).with_runtime_id("runtime-a"); + assert!( + FsRuntimeStore::migration_plan(&options) + .unwrap() + .migration_required + ); + FsRuntimeStore::migrate(&options).unwrap(); + let store = WorkerSessionStore::open_read_only(aggregate.join("session")).unwrap(); + let entries = store.read_all_read_only(session, segment).unwrap(); + assert_eq!(entries.len(), 6); + for (current, old) in entries[1..].iter().zip(source.lines()) { + let current = serde_json::to_value(current).unwrap(); + let old: Value = serde_json::from_str(old).unwrap(); + assert_eq!(current["ts"], old["ts"]); + assert_eq!(current["entry"]["item"], old["entry"]["item"]); + for field in ["entry_id", "origin", "derivation"] { + assert_eq!( + current["entry"]["metadata"][field], + old["entry"]["metadata"][field] + ); + } + if current["entry"]["metadata"]["origin"]["kind"] == "derived_summary" { + assert_eq!( + current["entry"]["metadata"]["input_metadata"]["usage"]["recording_material"], + "exclude" + ); + assert_eq!( + current["entry"]["metadata"]["input_metadata"]["usage"]["memory_evidence"], + "exclude" + ); + } + } + assert_eq!(fs::read(root.join("runtime.json")).unwrap(), owner_before); + assert_eq!( + fs::read(aggregate.join("worker.json")).unwrap(), + worker_before + ); + assert_eq!( + fs::read(aggregate.join("metadata.json")).unwrap(), + metadata_before + ); + let once = tree_bytes(&aggregate.join("session")); + assert!( + !FsRuntimeStore::migration_plan(&options) + .unwrap() + .migration_required + ); + FsRuntimeStore::migrate(&options).unwrap(); + assert_eq!(tree_bytes(&aggregate.join("session")), once); +} diff --git a/crates/worker-runtime/src/session_public.rs b/crates/worker-runtime/src/session_public.rs index 93f337f6d..ebdc2fa87 100644 --- a/crates/worker-runtime/src/session_public.rs +++ b/crates/worker-runtime/src/session_public.rs @@ -398,7 +398,7 @@ mod tests { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId(id.to_string()), origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".to_string(), diff --git a/crates/worker-runtime/src/worker_backend.rs b/crates/worker-runtime/src/worker_backend.rs index ca598cd80..b4c585cc1 100644 --- a/crates/worker-runtime/src/worker_backend.rs +++ b/crates/worker-runtime/src/worker_backend.rs @@ -5033,7 +5033,7 @@ mod tests { }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id, origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/worker/src/feature/builtin/journal.rs b/crates/worker/src/feature/builtin/journal.rs index 15d2bb2b4..e5de44038 100644 --- a/crates/worker/src/feature/builtin/journal.rs +++ b/crates/worker/src/feature/builtin/journal.rs @@ -79,8 +79,8 @@ fn journal_input( body: body.clone(), presentation: PublicSystemPresentation::Context, }; - let metadata = SystemInputMetadata::new( - SystemInputUsePolicy { + let metadata = HistoryInputMetadata::new( + HistoryInputUsePolicy { recording_material: InputEvidenceUse::Exclude, memory_evidence: InputEvidenceUse::Exclude, observation: InputObservationUse::Hidden, diff --git a/crates/worker/src/feature/builtin/memory.rs b/crates/worker/src/feature/builtin/memory.rs index ed90b8622..270de8430 100644 --- a/crates/worker/src/feature/builtin/memory.rs +++ b/crates/worker/src/feature/builtin/memory.rs @@ -61,12 +61,12 @@ fn resident_input( schema: &str, payload: &impl serde::Serialize, kind: &str, -) -> Result { +) -> Result { use session_store::*; let input = SystemInputEnvelope::new( body.clone(), - SystemInputMetadata::new( - SystemInputUsePolicy { + HistoryInputMetadata::new( + HistoryInputUsePolicy { recording_material: InputEvidenceUse::Exclude, memory_evidence: InputEvidenceUse::Exclude, observation: InputObservationUse::Hidden, @@ -93,7 +93,7 @@ pub(crate) fn subject_behavior_input( body: String, behavior_md: String, prompt_provenance: Option, -) -> Result { +) -> Result { resident_input( body, "subject:behavior", @@ -108,7 +108,7 @@ pub(crate) fn subject_behavior_input( pub(crate) fn resident_summary_input( body: String, prompt_provenance: Option, -) -> Result { +) -> Result { resident_input( body, "memory:resident-summary", @@ -891,7 +891,7 @@ impl SystemPromptContributionSource for WorkspaceSubjektivResidentSummarySource metadata: &crate::session_history::SessionHistoryMetadata, ) { if let Some(input) = metadata - .system_input + .input_metadata .as_ref() .and_then(|input| input.feature_payload()) .and_then(|payload| { diff --git a/crates/worker/src/feature/invocation.rs b/crates/worker/src/feature/invocation.rs index 91009461e..523be6068 100644 --- a/crates/worker/src/feature/invocation.rs +++ b/crates/worker/src/feature/invocation.rs @@ -48,15 +48,15 @@ pub(crate) struct InvocationResultInput { pub(crate) fn result_input( result: FeatureInvocationResult, body: String, -) -> Result { +) -> Result { use session_store::*; let payload = FeatureInputPayload::encode( "feature:invocation-result", 1, &InvocationResultInput { result }, )?; - let metadata = SystemInputMetadata::new( - SystemInputUsePolicy { + let metadata = HistoryInputMetadata::new( + HistoryInputUsePolicy { recording_material: InputEvidenceUse::Exclude, memory_evidence: InputEvidenceUse::Exclude, observation: InputObservationUse::Hidden, diff --git a/crates/worker/src/internal_worker.rs b/crates/worker/src/internal_worker.rs index 46400d52d..2c6a52a13 100644 --- a/crates/worker/src/internal_worker.rs +++ b/crates/worker/src/internal_worker.rs @@ -1499,7 +1499,7 @@ permission = "write" is_error: false, }, metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/worker/src/ipc/interceptor.rs b/crates/worker/src/ipc/interceptor.rs index 1e91f1797..95860b27b 100644 --- a/crates/worker/src/ipc/interceptor.rs +++ b/crates/worker/src/ipc/interceptor.rs @@ -1033,7 +1033,7 @@ mod tests { metadata: &crate::session_history::SessionHistoryMetadata, ) { if let Some(input) = metadata - .system_input + .input_metadata .as_ref() .and_then(|input| input.feature_payload()) .and_then(|payload| { diff --git a/crates/worker/src/session_capture.rs b/crates/worker/src/session_capture.rs index 7e78a12fc..e53da9105 100644 --- a/crates/worker/src/session_capture.rs +++ b/crates/worker/src/session_capture.rs @@ -352,11 +352,19 @@ impl SessionCapture { let entries = entries .into_iter() .map(|entry| { - let (item, use_policy) = if let Some(input) = &entry.annotation.system_input { - ( - Item::system_message(input.public_projection().body), - Some(input.public_use_policy()), - ) + let (item, use_policy) = if let Some(input) = &entry.annotation.input_metadata { + let item = if matches!( + entry.item, + Item::Message { + role: Role::System, + .. + } + ) { + Item::system_message(input.public_projection().body) + } else { + entry.item + }; + (item, Some(input.public_use_policy())) } else { (entry.item, None) }; @@ -982,10 +990,10 @@ mod tests { #[test] fn generic_public_input_observation_and_memory_policies_survive_store_restore_independently() { use session_store::{ - FeatureInputPayload, FsStore, InputCompactionUse, InputEvidenceUse, - InputObservationUse, LogEntry, LoggedSystemHistoryEntry, PublicSystemItem, - PublicSystemPresentation, Store, SystemInputEnvelope, SystemInputMetadata, - SystemInputUsePolicy, + FeatureInputPayload, FsStore, HistoryInputMetadata, HistoryInputUsePolicy, + InputCompactionUse, InputEvidenceUse, InputObservationUse, LogEntry, + LoggedSystemHistoryEntry, PublicSystemItem, PublicSystemPresentation, Store, + SystemInputEnvelope, }; let root = tempfile::tempdir().unwrap(); let store = FsStore::new(root.path()).unwrap(); @@ -1046,8 +1054,8 @@ mod tests { None, SystemInputEnvelope::new( "private model text", - SystemInputMetadata::new( - SystemInputUsePolicy { + HistoryInputMetadata::new( + HistoryInputUsePolicy { recording_material: InputEvidenceUse::Exclude, memory_evidence, observation, @@ -1121,6 +1129,152 @@ mod tests { } } + // Real FsStore seed/fork/reopen -> ordinary history replay, public snapshot, + // capture and both evidence consumers; no Feature or Tool execution occurs. + #[test] + fn derived_tool_context_keeps_roles_identity_and_policy_through_fork_without_becoming_evidence() + { + use session_store::public_material::{ + PublicMaterialLimits, PublicMaterialUse, SessionPublicMaterialReader, + }; + use session_store::*; + let root = tempfile::tempdir().unwrap(); + let store = FsStore::new(root.path()).unwrap(); + let session = new_session_id(); + let first = new_segment_id(); + let fork = new_segment_id(); + let items = vec![ + Item::tool_call("original", "ArbitraryTool", "{}"), + Item::tool_result("original", "original result"), + Item::tool_call("derived", "ArbitraryTool", "PRIVATE_SYNTHETIC_CONTEXT"), + Item::tool_result("derived", "PRIVATE_SYNTHETIC_RESULT"), + ]; + let mut history: Vec<_> = items + .iter() + .cloned() + .map(|item| LoggedHistoryEntry { + item: LoggedItem::from(item), + metadata: LoggedSessionHistoryMetadata::legacy_unknown(), + }) + .collect(); + let sources = history[..2] + .iter() + .map(|entry| entry.metadata.entry_id.clone()) + .collect::>(); + for entry in &mut history[2..] { + entry.metadata.derivation = Some(LoggedHistoryDerivation { + sources: sources.clone(), + }); + entry.metadata.input_metadata = Some( + HistoryInputMetadata::new( + HistoryInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::Retain, + }, + None, + None, + ) + .unwrap(), + ); + } + for segment in [first, fork] { + store + .create_segment( + session, + segment, + &[LogEntry::AnnotatedSegmentStart { + ts: 1, + session_id: session, + system_prompt: None, + config: Default::default(), + history: history.clone(), + forked_from: (segment == fork).then_some(SegmentOrigin { + segment_id: first, + at_turn_index: 0, + }), + compacted_from: None, + }], + ) + .unwrap(); + } + let reopened = FsStore::new(root.path()).unwrap(); + let saved = reopened.read_all(session, fork).unwrap(); + let restored = + crate::session_history::restore_history_entries(session, fork, &saved).unwrap(); + for (index, entry) in restored.iter().enumerate() { + assert_eq!(entry.item, items[index]); + assert_eq!(entry.annotation, history[index].metadata); + } + let capture = SessionCapture::from_history_entries(fork.to_string(), restored); + assert!( + matches!(&capture.entries[2].item, Item::ToolCall { call_id, .. } if call_id == "derived") + ); + assert!( + matches!(&capture.entries[3].item, Item::ToolResult { call_id, .. } if call_id == "derived") + ); + let snapshot = public_snapshot::project_session_snapshot(session, &saved); + assert!( + !serde_json::to_string(&snapshot) + .unwrap() + .contains("PRIVATE_SYNTHETIC") + ); + for capture in [ + capture, + SessionCapture::from_session_snapshot(fork.to_string(), snapshot), + ] { + assert_eq!( + capture + .search(&SearchOptions { + kind: Some(ReferenceKind::Tool), + ..Default::default() + }) + .len(), + 2 + ); + for (index, entry) in history.iter().enumerate() { + let reference = SessionEntryRef::from_history_entry_id(&entry.metadata.entry_id); + assert_eq!( + !capture + .read(ReadSelector::Id(reference.as_str()), ReadOptions::default()) + .entries + .is_empty(), + index < 2 + ); + assert_eq!( + capture.evidence_for(reference.as_str()).is_some(), + index < 2 + ); + } + } + for purpose in [ + PublicMaterialUse::Recording, + PublicMaterialUse::MemoryEvidence, + ] { + let reader = SessionPublicMaterialReader::open_fs_for_use( + root.path(), + session, + fork, + PublicMaterialLimits::default(), + reopened.retention(), + purpose, + ) + .unwrap(); + let ids = reader + .descriptors() + .map(|entry| entry.identity.entry_ref.clone()) + .collect::>(); + assert_eq!( + ids, + sources + .iter() + .map(|id| format!("E{}", id.0)) + .collect::>() + ); + } + } + #[test] fn overview_contains_user_and_assistant_only() { let view = SessionCapture::new( diff --git a/crates/worker/src/session_history.rs b/crates/worker/src/session_history.rs index 472d40eb6..dda081a38 100644 --- a/crates/worker/src/session_history.rs +++ b/crates/worker/src/session_history.rs @@ -24,7 +24,7 @@ pub(crate) fn test_logged_history_entry(item: impl Into) -> LoggedHistoryE LoggedHistoryEntry { item: session_store::LoggedItem::from(item.into()), metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -39,7 +39,7 @@ pub(crate) fn test_logged_system_entry( session_store::LoggedSystemHistoryEntry { item, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId::new(), origin: LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -60,7 +60,7 @@ pub(crate) fn metadata( derivation: Option, ) -> SessionHistoryMetadata { SessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: SessionHistoryEntryId::new(), origin, derivation, @@ -71,7 +71,7 @@ pub(crate) fn compaction_use( metadata: &SessionHistoryMetadata, ) -> session_store::InputCompactionUse { metadata - .system_input + .input_metadata .as_ref() .map(|input| input.usage().compaction) .unwrap_or(session_store::InputCompactionUse::Retain) @@ -93,7 +93,7 @@ pub(crate) fn history_entry_with_id( HistoryEntry::new( item, SessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id, origin, derivation: None, diff --git a/crates/worker/src/skill.rs b/crates/worker/src/skill.rs index 4629e03a8..2ad347a6d 100644 --- a/crates/worker/src/skill.rs +++ b/crates/worker/src/skill.rs @@ -16,7 +16,7 @@ pub(crate) struct SkillActivationInput { pub(crate) fn activation_input( activation: &SkillActivationResponse, -) -> Result { +) -> Result { use session_store::*; let body = format!( "Agent Skill `{}` activated from {}.\n\n{}", @@ -29,8 +29,8 @@ pub(crate) fn activation_input( name: activation.name.clone(), }, )?; - let metadata = SystemInputMetadata::new( - SystemInputUsePolicy { + let metadata = HistoryInputMetadata::new( + HistoryInputUsePolicy { recording_material: InputEvidenceUse::Exclude, memory_evidence: InputEvidenceUse::Exclude, observation: InputObservationUse::Hidden, diff --git a/crates/worker/src/spawn/registry.rs b/crates/worker/src/spawn/registry.rs index 1d5b7dfb3..9315c3532 100644 --- a/crates/worker/src/spawn/registry.rs +++ b/crates/worker/src/spawn/registry.rs @@ -1338,7 +1338,7 @@ mod tests { is_error: false, }, metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId(entry_id.into()), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/worker/src/subjektiv.rs b/crates/worker/src/subjektiv.rs index eb871f992..0593e00ad 100644 --- a/crates/worker/src/subjektiv.rs +++ b/crates/worker/src/subjektiv.rs @@ -1156,7 +1156,7 @@ enabled = true LogEntry::AnnotatedSystemItem { entry, .. } if entry .metadata - .system_input + .input_metadata .as_ref() .and_then(|input| input.feature_payload()) .is_some_and(|payload| payload.schema() == "journal:startup") => @@ -1178,7 +1178,7 @@ enabled = true }; let payload = journal .metadata - .system_input + .input_metadata .as_ref() .unwrap() .feature_payload() @@ -1277,7 +1277,7 @@ enabled = true } assert_eq!(host.startup_calls.load(Ordering::SeqCst), 1); let logs = store.read_all(session, segment).unwrap(); - assert_eq!(logs.iter().filter(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } if entry.metadata.system_input.as_ref().and_then(|input| input.feature_payload()).is_some_and(|payload| payload.schema() == "journal:startup"))).count(), usize::from(contributes_context)); + assert_eq!(logs.iter().filter(|entry| matches!(entry, LogEntry::AnnotatedSystemItem { entry, .. } if entry.metadata.input_metadata.as_ref().and_then(|input| input.feature_payload()).is_some_and(|payload| payload.schema() == "journal:startup"))).count(), usize::from(contributes_context)); controller .handle .send(Method::Shutdown { diff --git a/crates/worker/src/wip.rs b/crates/worker/src/wip.rs index 1f12478d3..0965571bf 100644 --- a/crates/worker/src/wip.rs +++ b/crates/worker/src/wip.rs @@ -5502,7 +5502,7 @@ mod tests { entry: session_store::LoggedHistoryEntry { item: session_store::LoggedItem::from(saved), metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId::new(), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index aabb6314f..0e29f0cbd 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -2739,7 +2739,7 @@ pub trait SystemItemCommitter: Send + Sync { extensions: Vec, after_commit: &mut dyn FnMut(&HistoryEntry), ) -> Result, StoreError> { - entry.validate_system_input()?; + entry.validate_input_metadata()?; let history = HistoryEntry::new(entry.item.to_history_item(), entry.metadata.clone()); self.commit_usage_boundary( LogEntry::AnnotatedSystemItem { @@ -6950,7 +6950,7 @@ impl Worker { }; for entry in &frozen_context { entry - .validate_system_input() + .validate_input_metadata() .map_err(|e| WorkerError::InvalidState(e.to_string()))?; } let history_entries = self.session.history().entries(); @@ -7469,8 +7469,8 @@ impl Worker { new_history[0] .as_text() .expect("compaction summary is text"), - session_store::SystemInputMetadata::new( - session_store::SystemInputUsePolicy { + session_store::HistoryInputMetadata::new( + session_store::HistoryInputUsePolicy { recording_material: session_store::InputEvidenceUse::Exclude, memory_evidence: session_store::InputEvidenceUse::Exclude, observation: session_store::InputObservationUse::Hidden, @@ -7509,15 +7509,26 @@ impl Worker { original_cursor += offset + 1; HistoryEntry::new(item, original.annotation.clone()) } else { - HistoryEntry::new( - item, - new_history_metadata( - WorkerHistoryProvenance::DerivedSummary, - Some(SessionHistoryDerivation { - sources: derived_sources.clone(), - }), - ), - ) + let mut metadata = new_history_metadata( + WorkerHistoryProvenance::DerivedSummary, + Some(SessionHistoryDerivation { + sources: derived_sources.clone(), + }), + ); + metadata.input_metadata = Some( + session_store::HistoryInputMetadata::new( + session_store::HistoryInputUsePolicy { + recording_material: session_store::InputEvidenceUse::Exclude, + memory_evidence: session_store::InputEvidenceUse::Exclude, + observation: session_store::InputObservationUse::Hidden, + compaction: session_store::InputCompactionUse::Retain, + }, + None, + None, + ) + .expect("bounded private derived context policy"), + ); + HistoryEntry::new(item, metadata) } }) .collect::>(); @@ -10790,9 +10801,9 @@ mod build_summary_prompt_tests { producer: "arbitrary-summary-source".into(), }, ); - entry.annotation.system_input = Some( - session_store::SystemInputMetadata::new( - session_store::SystemInputUsePolicy { + entry.annotation.input_metadata = Some( + session_store::HistoryInputMetadata::new( + session_store::HistoryInputUsePolicy { recording_material: session_store::InputEvidenceUse::Exclude, memory_evidence: session_store::InputEvidenceUse::Exclude, observation: session_store::InputObservationUse::Hidden, @@ -14631,7 +14642,7 @@ permission = "write" matches!( entry, LogEntry::AnnotatedSystemItem { entry, .. } - if entry.metadata.system_input.as_ref().and_then(|input| input.feature_payload()).and_then(|payload| payload.decode::("skill:activation", 1).ok()).is_some_and(|payload| payload.name == "triage-errors") + if entry.metadata.input_metadata.as_ref().and_then(|input| input.feature_payload()).and_then(|payload| payload.decode::("skill:activation", 1).ok()).is_some_and(|payload| payload.name == "triage-errors") && entry.item.history_text().contains("# Triage Errors") && entry.item.history_text() == history_text ) @@ -16919,7 +16930,7 @@ permission = "write" fn input_schema(entry: &session_store::LoggedSystemHistoryEntry) -> Option<&str> { entry .metadata - .system_input + .input_metadata .as_ref()? .feature_payload() .map(|payload| payload.schema()) @@ -16953,8 +16964,8 @@ permission = "write" None, SystemInputEnvelope::new( "PRIOR_JOURNAL_REFERENCE", - SystemInputMetadata::new( - SystemInputUsePolicy { + HistoryInputMetadata::new( + HistoryInputUsePolicy { recording_material: InputEvidenceUse::Exclude, memory_evidence: InputEvidenceUse::Exclude, observation: InputObservationUse::Hidden, diff --git a/crates/worker/tests/compact_events_test.rs b/crates/worker/tests/compact_events_test.rs index e0f773abf..faeea461b 100644 --- a/crates/worker/tests/compact_events_test.rs +++ b/crates/worker/tests/compact_events_test.rs @@ -119,7 +119,7 @@ fn annotated(item: Item) -> session_store::LoggedHistoryEntry { session_store::LoggedHistoryEntry { item: session_store::LoggedItem::from(item), metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId::new(), origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown, derivation: None, @@ -2220,7 +2220,7 @@ fn summary_entry( origin: session_store::LoggedSessionHistoryOrigin, ) -> session_store::LoggedHistoryEntry { let mut entry = annotated(item); - entry.metadata.system_input = matches!( + entry.metadata.input_metadata = matches!( &entry.item, session_store::LoggedItem::Message { role: session_store::LoggedRole::System, @@ -2228,8 +2228,8 @@ fn summary_entry( } ) .then(|| { - session_store::SystemInputMetadata::new( - session_store::SystemInputUsePolicy { + session_store::HistoryInputMetadata::new( + session_store::HistoryInputUsePolicy { recording_material: session_store::InputEvidenceUse::Exclude, memory_evidence: session_store::InputEvidenceUse::Exclude, observation: session_store::InputObservationUse::Hidden, @@ -2256,7 +2256,7 @@ fn compaction_use( metadata: &session_store::LoggedSessionHistoryMetadata, ) -> session_store::InputCompactionUse { metadata - .system_input + .input_metadata .as_ref() .map(|input| input.usage().compaction) .unwrap_or(session_store::InputCompactionUse::Retain) @@ -3167,8 +3167,8 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai None, session_store::SystemInputEnvelope::new( "OLD_TYPED_STARTUP_JOURNAL", - session_store::SystemInputMetadata::new( - session_store::SystemInputUsePolicy { + session_store::HistoryInputMetadata::new( + session_store::HistoryInputUsePolicy { recording_material: session_store::InputEvidenceUse::Exclude, memory_evidence: session_store::InputEvidenceUse::Exclude, observation: session_store::InputObservationUse::Hidden, diff --git a/crates/worker/tests/feature_invocation_test.rs b/crates/worker/tests/feature_invocation_test.rs index 805f694ac..91f3969b7 100644 --- a/crates/worker/tests/feature_invocation_test.rs +++ b/crates/worker/tests/feature_invocation_test.rs @@ -231,7 +231,7 @@ fn results(worker: &Worker) -> Vec { } entry .metadata - .system_input + .input_metadata .as_ref()? .feature_payload()? .decode::("feature:invocation-result", 1) diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 4bdf8a7f6..4ffb70865 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -44108,7 +44108,7 @@ mod tests { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId(id.to_string()), origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".to_string(), @@ -44298,7 +44298,7 @@ mod tests { content: vec![LoggedContentPart::Text { text: text.into() }], }, metadata: LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: LoggedSessionHistoryEntryId(id.to_string()), origin: LoggedSessionHistoryOrigin::HumanInput { account_id: "account-1".to_string(), @@ -48550,21 +48550,25 @@ mod tests { let worker_root = root .join(".test-embedded-runtime-store/workers") .join(&worker.worker_id); - fs::create_dir_all(worker_root.join("session/segments")).unwrap(); - fs::write( - worker_root.join("session/session.json"), - serde_json::to_vec_pretty(&json!({ - "schema_version": 1, - "session_id": session_store::new_session_id() - })) - .unwrap(), - ) - .unwrap(); - fs::write( - worker_root.join("session/segments/segment-a.jsonl"), - b"backend job retained session evidence\n", - ) - .unwrap(); + use session_store::Store; + let store = session_store::WorkerSessionStore::new(worker_root.join("session")).unwrap(); + let session = session_store::new_session_id(); + let segment = session_store::new_segment_id(); + store + .create_segment( + session, + segment, + &[session_store::LogEntry::AnnotatedSegmentStart { + ts: 1, + session_id: session, + system_prompt: Some("backend job retained session evidence".into()), + config: Default::default(), + history: vec![], + forked_from: None, + compacted_from: None, + }], + ) + .unwrap(); } fn job_notification_evidence( diff --git a/crates/workspace-server/src/server/tests/subjektiv_journal_connected_tests.rs b/crates/workspace-server/src/server/tests/subjektiv_journal_connected_tests.rs index 893720ccd..a1447eece 100644 --- a/crates/workspace-server/src/server/tests/subjektiv_journal_connected_tests.rs +++ b/crates/workspace-server/src/server/tests/subjektiv_journal_connected_tests.rs @@ -562,7 +562,7 @@ async fn backend_real_runtime_journal_release_and_next_body_persisted_startup() }; let payload = entry .metadata - .system_input + .input_metadata .as_ref()? .feature_payload()? .decode::("journal:startup", 1) diff --git a/crates/workspace-server/src/server/tests/subjektiv_journal_tests.rs b/crates/workspace-server/src/server/tests/subjektiv_journal_tests.rs index fd0c89fea..4b0500eb8 100644 --- a/crates/workspace-server/src/server/tests/subjektiv_journal_tests.rs +++ b/crates/workspace-server/src/server/tests/subjektiv_journal_tests.rs @@ -479,7 +479,7 @@ fn seed_session( vec![session_store::LoggedHistoryEntry { item, metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId("read-source-original".into()), origin, derivation: None, @@ -1707,7 +1707,7 @@ impl SubjectRemovalFixture { .map(|text| session_store::LoggedHistoryEntry { item: message(text), metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId(text.into()), origin: session_store::LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, diff --git a/crates/workspace-server/src/server/tests/subjektiv_memory_journal_tests.rs b/crates/workspace-server/src/server/tests/subjektiv_memory_journal_tests.rs index efe44156f..190924f52 100644 --- a/crates/workspace-server/src/server/tests/subjektiv_memory_journal_tests.rs +++ b/crates/workspace-server/src/server/tests/subjektiv_memory_journal_tests.rs @@ -67,7 +67,7 @@ impl Fixture { content: vec![session_store::LoggedContentPart::Text { text: text.into() }], }, metadata: session_store::LoggedSessionHistoryMetadata { - system_input: None, + input_metadata: None, entry_id: session_store::LoggedSessionHistoryEntryId(format!("original-{index}")), origin: session_store::LoggedSessionHistoryOrigin::LocalHumanInput, derivation: None, From 8017c55d82b990321f2b103da54bdc18bcd41fa4 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 20 Oct 2026 05:34:22 +0900 Subject: [PATCH 11/11] fix: retain annotated compaction history and honor non-System input policy --- crates/session-store/src/input_metadata.rs | 17 +- crates/session-store/src/public_material.rs | 3 +- crates/session-store/src/public_snapshot.rs | 42 ++- crates/worker/src/session_capture.rs | 319 +++++++++++--------- crates/worker/src/worker.rs | 195 ++++++------ crates/worker/tests/compact_events_test.rs | 129 ++++++++ 6 files changed, 446 insertions(+), 259 deletions(-) diff --git a/crates/session-store/src/input_metadata.rs b/crates/session-store/src/input_metadata.rs index 2d29d07da..eeed42b9c 100644 --- a/crates/session-store/src/input_metadata.rs +++ b/crates/session-store/src/input_metadata.rs @@ -238,11 +238,20 @@ impl HistoryInputMetadata { } pub fn public_use_policy(&self) -> protocol::SessionEntryUsePolicy { - let has_public = self.public.is_some(); protocol::SessionEntryUsePolicy { - observation_readable: has_public - && self.usage.observation == InputObservationUse::Readable, - memory_evidence: has_public && self.usage.memory_evidence == InputEvidenceUse::Include, + observation_readable: self.usage.observation == InputObservationUse::Readable, + memory_evidence: self.usage.memory_evidence == InputEvidenceUse::Include, + } + } + + pub fn public_system_use_policy(&self) -> protocol::SessionEntryUsePolicy { + if self.public.is_some() { + self.public_use_policy() + } else { + protocol::SessionEntryUsePolicy { + observation_readable: false, + memory_evidence: false, + } } } diff --git a/crates/session-store/src/public_material.rs b/crates/session-store/src/public_material.rs index a7cdccaf2..150852174 100644 --- a/crates/session-store/src/public_material.rs +++ b/crates/session-store/src/public_material.rs @@ -903,12 +903,12 @@ fn canonical_body( if allowed == crate::InputEvidenceUse::Exclude { return None; } - let public = input.public_item()?; if let LoggedItem::Message { role: LoggedRole::System, .. } = &entry.item { + let public = input.public_item()?; return Some(CanonicalBody::Message { origin: &entry.metadata.origin, role: LoggedRole::System, @@ -917,7 +917,6 @@ fn canonical_body( }]), }); } - return None; } match &entry.item { LoggedItem::Message { diff --git a/crates/session-store/src/public_snapshot.rs b/crates/session-store/src/public_snapshot.rs index ce23ee221..cf03cf752 100644 --- a/crates/session-store/src/public_snapshot.rs +++ b/crates/session-store/src/public_snapshot.rs @@ -1627,7 +1627,7 @@ pub(crate) fn project_session_snapshot_for_segment( .metadata .input_metadata .as_ref() - .map(crate::HistoryInputMetadata::public_use_policy), + .map(crate::HistoryInputMetadata::public_system_use_policy), )), LogEntry::RunYielded { ts, @@ -1795,11 +1795,19 @@ fn history_entry( data: SessionSnapshotEntryData, ) -> SessionSnapshotEntry { SessionSnapshotEntry { - use_policy: entry - .metadata - .input_metadata - .as_ref() - .map(crate::HistoryInputMetadata::public_use_policy), + use_policy: entry.metadata.input_metadata.as_ref().map(|input| { + if matches!( + entry.item, + LoggedItem::Message { + role: LoggedRole::System, + .. + } + ) { + input.public_system_use_policy() + } else { + input.public_use_policy() + } + }), entry_id: entry.metadata.entry_id.0.clone(), timestamp, provenance: provenance(&entry.metadata.origin), @@ -1927,21 +1935,29 @@ pub fn session_tool_attachment_id( fn project_history_item(entry: &LoggedHistoryEntry) -> Option { if let Some(input) = &entry.metadata.input_metadata { - if !matches!( + if matches!( entry.item, LoggedItem::Message { role: LoggedRole::System, .. } ) { + let public = input.public_projection(); + return Some(SessionSnapshotEntryData::SystemItem { + item_kind: public.kind.clone(), + content: public.body.clone(), + data: Some( + serde_json::to_value(public).expect("public system input is serializable"), + ), + }); + } + let usage = input.usage(); + if usage.recording_material == crate::InputEvidenceUse::Exclude + && usage.memory_evidence == crate::InputEvidenceUse::Exclude + && usage.observation == crate::InputObservationUse::Hidden + { return None; } - let public = input.public_projection(); - return Some(SessionSnapshotEntryData::SystemItem { - item_kind: public.kind.clone(), - content: public.body.clone(), - data: Some(serde_json::to_value(public).expect("public system input is serializable")), - }); } project_item(&entry.metadata.entry_id.0, &entry.item) } diff --git a/crates/worker/src/session_capture.rs b/crates/worker/src/session_capture.rs index e53da9105..c255d7dad 100644 --- a/crates/worker/src/session_capture.rs +++ b/crates/worker/src/session_capture.rs @@ -353,18 +353,21 @@ impl SessionCapture { .into_iter() .map(|entry| { let (item, use_policy) = if let Some(input) = &entry.annotation.input_metadata { - let item = if matches!( + let is_system = matches!( entry.item, Item::Message { role: Role::System, .. } - ) { - Item::system_message(input.public_projection().body) + ); + if is_system { + ( + Item::system_message(input.public_projection().body), + Some(input.public_system_use_policy()), + ) } else { - entry.item - }; - (item, Some(input.public_use_policy())) + (entry.item, Some(input.public_use_policy())) + } } else { (entry.item, None) }; @@ -1132,147 +1135,181 @@ mod tests { // Real FsStore seed/fork/reopen -> ordinary history replay, public snapshot, // capture and both evidence consumers; no Feature or Tool execution occurs. #[test] - fn derived_tool_context_keeps_roles_identity_and_policy_through_fork_without_becoming_evidence() - { + fn tool_policy_axes_survive_restore_and_fork_without_exposing_derived_context() { use session_store::public_material::{ PublicMaterialLimits, PublicMaterialUse, SessionPublicMaterialReader, }; use session_store::*; - let root = tempfile::tempdir().unwrap(); - let store = FsStore::new(root.path()).unwrap(); - let session = new_session_id(); - let first = new_segment_id(); - let fork = new_segment_id(); - let items = vec![ - Item::tool_call("original", "ArbitraryTool", "{}"), - Item::tool_result("original", "original result"), - Item::tool_call("derived", "ArbitraryTool", "PRIVATE_SYNTHETIC_CONTEXT"), - Item::tool_result("derived", "PRIVATE_SYNTHETIC_RESULT"), - ]; - let mut history: Vec<_> = items - .iter() - .cloned() - .map(|item| LoggedHistoryEntry { - item: LoggedItem::from(item), - metadata: LoggedSessionHistoryMetadata::legacy_unknown(), - }) - .collect(); - let sources = history[..2] - .iter() - .map(|entry| entry.metadata.entry_id.clone()) - .collect::>(); - for entry in &mut history[2..] { - entry.metadata.derivation = Some(LoggedHistoryDerivation { - sources: sources.clone(), - }); - entry.metadata.input_metadata = Some( - HistoryInputMetadata::new( - HistoryInputUsePolicy { - recording_material: InputEvidenceUse::Exclude, - memory_evidence: InputEvidenceUse::Exclude, - observation: InputObservationUse::Hidden, - compaction: InputCompactionUse::Retain, - }, - None, - None, - ) - .unwrap(), - ); - } - for segment in [first, fork] { - store - .create_segment( - session, - segment, - &[LogEntry::AnnotatedSegmentStart { - ts: 1, - session_id: session, - system_prompt: None, - config: Default::default(), - history: history.clone(), - forked_from: (segment == fork).then_some(SegmentOrigin { - segment_id: first, - at_turn_index: 0, - }), - compacted_from: None, - }], - ) - .unwrap(); - } - let reopened = FsStore::new(root.path()).unwrap(); - let saved = reopened.read_all(session, fork).unwrap(); - let restored = - crate::session_history::restore_history_entries(session, fork, &saved).unwrap(); - for (index, entry) in restored.iter().enumerate() { - assert_eq!(entry.item, items[index]); - assert_eq!(entry.annotation, history[index].metadata); - } - let capture = SessionCapture::from_history_entries(fork.to_string(), restored); - assert!( - matches!(&capture.entries[2].item, Item::ToolCall { call_id, .. } if call_id == "derived") - ); - assert!( - matches!(&capture.entries[3].item, Item::ToolResult { call_id, .. } if call_id == "derived") - ); - let snapshot = public_snapshot::project_session_snapshot(session, &saved); - assert!( - !serde_json::to_string(&snapshot) - .unwrap() - .contains("PRIVATE_SYNTHETIC") - ); - for capture in [ - capture, - SessionCapture::from_session_snapshot(fork.to_string(), snapshot), - ] { - assert_eq!( - capture - .search(&SearchOptions { - kind: Some(ReferenceKind::Tool), - ..Default::default() - }) - .len(), - 2 - ); - for (index, entry) in history.iter().enumerate() { - let reference = SessionEntryRef::from_history_entry_id(&entry.metadata.entry_id); - assert_eq!( - !capture - .read(ReadSelector::Id(reference.as_str()), ReadOptions::default()) - .entries - .is_empty(), - index < 2 - ); - assert_eq!( - capture.evidence_for(reference.as_str()).is_some(), - index < 2 - ); + for recording in [InputEvidenceUse::Include, InputEvidenceUse::Exclude] { + for memory in [InputEvidenceUse::Include, InputEvidenceUse::Exclude] { + for observation in [InputObservationUse::Readable, InputObservationUse::Hidden] { + let root = tempfile::tempdir().unwrap(); + let store = FsStore::new(root.path()).unwrap(); + let session = new_session_id(); + let first = new_segment_id(); + let fork = new_segment_id(); + let items = vec![ + Item::tool_call("original", "ArbitraryTool", "{}"), + Item::tool_result("original", "original result"), + Item::tool_call("derived", "ArbitraryTool", "PRIVATE_SYNTHETIC_CONTEXT"), + Item::tool_result("derived", "PRIVATE_SYNTHETIC_RESULT"), + ]; + let mut history: Vec<_> = items + .iter() + .cloned() + .map(|item| LoggedHistoryEntry { + item: LoggedItem::from(item), + metadata: LoggedSessionHistoryMetadata::legacy_unknown(), + }) + .collect(); + let sources = history[..2] + .iter() + .map(|entry| entry.metadata.entry_id.clone()) + .collect::>(); + for entry in &mut history[..2] { + entry.metadata.input_metadata = Some( + HistoryInputMetadata::new( + HistoryInputUsePolicy { + recording_material: recording, + memory_evidence: memory, + observation, + compaction: InputCompactionUse::Retain, + }, + None, + None, + ) + .unwrap(), + ); + } + for entry in &mut history[2..] { + entry.metadata.derivation = Some(LoggedHistoryDerivation { + sources: sources.clone(), + }); + entry.metadata.input_metadata = Some( + HistoryInputMetadata::new( + HistoryInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction: InputCompactionUse::Retain, + }, + None, + None, + ) + .unwrap(), + ); + } + for segment in [first, fork] { + store + .create_segment( + session, + segment, + &[LogEntry::AnnotatedSegmentStart { + ts: 1, + session_id: session, + system_prompt: None, + config: Default::default(), + history: history.clone(), + forked_from: (segment == fork).then_some(SegmentOrigin { + segment_id: first, + at_turn_index: 0, + }), + compacted_from: None, + }], + ) + .unwrap(); + } + let reopened = FsStore::new(root.path()).unwrap(); + let saved = reopened.read_all(session, fork).unwrap(); + let restored = + crate::session_history::restore_history_entries(session, fork, &saved) + .unwrap(); + for (index, entry) in restored.iter().enumerate() { + assert_eq!(entry.item, items[index]); + assert_eq!(entry.annotation, history[index].metadata); + } + let capture = SessionCapture::from_history_entries(fork.to_string(), restored); + assert!( + matches!(&capture.entries[2].item, Item::ToolCall { call_id, .. } if call_id == "derived") + ); + assert!( + matches!(&capture.entries[3].item, Item::ToolResult { call_id, .. } if call_id == "derived") + ); + let snapshot = public_snapshot::project_session_snapshot(session, &saved); + assert!( + !serde_json::to_string(&snapshot) + .unwrap() + .contains("PRIVATE_SYNTHETIC") + ); + for capture in [ + capture, + SessionCapture::from_session_snapshot(fork.to_string(), snapshot), + ] { + assert_eq!( + capture + .search(&SearchOptions { + kind: Some(ReferenceKind::Tool), + ..Default::default() + }) + .len(), + if observation == InputObservationUse::Readable { + 2 + } else { + 0 + } + ); + for (index, entry) in history.iter().enumerate() { + let reference = + SessionEntryRef::from_history_entry_id(&entry.metadata.entry_id); + assert_eq!( + !capture + .read( + ReadSelector::Id(reference.as_str()), + ReadOptions::default() + ) + .entries + .is_empty(), + index < 2 && observation == InputObservationUse::Readable + ); + assert_eq!( + capture.evidence_for(reference.as_str()).is_some(), + index < 2 && memory == InputEvidenceUse::Include + ); + } + } + for purpose in [ + PublicMaterialUse::Recording, + PublicMaterialUse::MemoryEvidence, + ] { + let reader = SessionPublicMaterialReader::open_fs_for_use( + root.path(), + session, + fork, + PublicMaterialLimits::default(), + reopened.retention(), + purpose, + ) + .unwrap(); + let ids = reader + .descriptors() + .map(|entry| entry.identity.entry_ref.clone()) + .collect::>(); + let included = match purpose { + PublicMaterialUse::Recording => recording, + PublicMaterialUse::MemoryEvidence => memory, + } == InputEvidenceUse::Include; + assert_eq!( + ids, + sources + .iter() + .filter(|_| included) + .map(|id| format!("E{}", id.0)) + .collect::>() + ); + } + } } } - for purpose in [ - PublicMaterialUse::Recording, - PublicMaterialUse::MemoryEvidence, - ] { - let reader = SessionPublicMaterialReader::open_fs_for_use( - root.path(), - session, - fork, - PublicMaterialLimits::default(), - reopened.retention(), - purpose, - ) - .unwrap(); - let ids = reader - .descriptors() - .map(|entry| entry.identity.entry_ref.clone()) - .collect::>(); - assert_eq!( - ids, - sources - .iter() - .map(|id| format!("E{}", id.0)) - .collect::>() - ); - } } #[test] diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 09d2159ff..955835c1b 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -6959,7 +6959,7 @@ impl Worker { // The old summary is consumed in full by the compaction service even when // the cut falls before it. The replacement summary supersedes it: retaining // it as well would duplicate inherited context on every compact. - let retained_history_entries = history_entries[retain_from..] + let mut retained_history_entries = history_entries[retain_from..] .iter() .filter(|entry| { crate::session_history::compaction_use(&entry.annotation) @@ -6967,16 +6967,16 @@ impl Worker { }) .cloned() .collect::>(); - let mut retained_items = retained_history_entries - .iter() - .map(|entry| entry.item.clone()) - .collect::>(); let default_workdir_alias = (self.workdir_sessions.len() == 1) .then(|| self.workdir_sessions.aliases().into_iter().next()) .flatten() .map(|alias| alias.as_str().to_string()); - let retained_image_references = - project_compaction_attachments(&mut retained_items, default_workdir_alias.as_deref()); + let retained_image_references = project_compaction_attachments( + retained_history_entries + .iter_mut() + .map(|entry| &mut entry.item), + default_workdir_alias.as_deref(), + ); // The compactor explores summaries, text details, and ViewImage call arguments. It // never needs durable image bodies; relevant images are nominated with add_reference. let mut entries_to_summarise = history_entries[..retain_from] @@ -7326,8 +7326,14 @@ impl Worker { Vec::new() }; let auto_read_tokens = agen::token_counter::total_tokens(&auto_read_messages, &[]).tokens; - let retained_estimate = agen::token_counter::total_tokens(&retained_items, &[]); - let retained_item_count = u64::try_from(retained_items.len()).unwrap_or(u64::MAX); + let retained_estimate = agen::token_counter::total_tokens( + &retained_history_entries + .iter() + .map(|entry| entry.item.clone()) + .collect::>(), + &[], + ); + let retained_item_count = u64::try_from(retained_history_entries.len()).unwrap_or(u64::MAX); let summarized_item_count = u64::try_from(items_to_summarise.len()).unwrap_or(u64::MAX); let compactor_entries = handle.entries(); let compactor_turns = compactor_entries @@ -7382,25 +7388,19 @@ impl Worker { let reference_message = build_related_file_message(&final_ctx.references, &retained_image_references); - // Count surviving user_messages before consuming `retained_items` + // Count surviving user_messages before consuming the annotated retained tail // — needed to align `self.user_segments` after the swap below. - let retained_user_msgs = retained_items + let retained_user_msgs = retained_history_entries .iter() - .filter(|i| i.is_user_message()) + .filter(|entry| entry.item.is_user_message()) .count(); // Build new history: [summary, ...auto-read, references, ...retained, task snapshot, TaskList synthetic call/result]. // The TaskStore snapshot trails the retained items so that, on resume, // `replay_history` walks any pre-compact Task* calls preserved verbatim - // in retained_items first and the trailing snapshot's `replace_with` + // in the retained tail first and the trailing snapshot's `replace_with` // is the final word — pre-compact `TaskCreate` calls cannot leak as // duplicate entries. - let mut new_history = Vec::with_capacity( - 1 + auto_read_messages.len() - + 3 - + reference_message.is_some() as usize - + retained_items.len(), - ); let mut compact_introduced_system_messages = Vec::with_capacity(2 + auto_read_messages.len() + reference_message.is_some() as usize); let summary_message = Item::system_message(format!( @@ -7424,49 +7424,13 @@ impl Worker { )); compact_introduced_system_messages.push(task_snapshot_message.clone()); - new_history.push(summary_message); - new_history.extend(context_messages); - new_history.extend(auto_read_messages); - if let Some(msg) = reference_message { - new_history.push(msg); - } - new_history.extend(retained_items); - new_history.push(task_snapshot_message); - new_history.push(Item::tool_call("compact-tasklist", "TaskList", "{}")); - new_history.push(Item::tool_result_with_content( - "compact-tasklist", - self.task_feature.snapshot_overview(), - task_snapshot_text.clone(), - )); - let result_estimate = agen::token_counter::total_tokens(&new_history, &[]); - if result_context_max_tokens > 0 && result_estimate.tokens > result_context_max_tokens { - // This history estimate includes retained items and auto-read files, not - // just the summary. Treat the configured result target as advisory; - // request-time context safety checks still apply after activation. - warn!( - result_tokens = result_estimate.tokens, - warning_threshold = result_context_max_tokens, - retained_tokens = retained_estimate.tokens, - summary_tokens, - auto_read_tokens, - "compacted result context exceeds warning threshold; continuing" - ); - self.alert( - AlertLevel::Warn, - AlertSource::Compactor, - format!( - "compacted result context is larger than expected (≈{} tokens; warning threshold {}); continuing compaction", - result_estimate.tokens, result_context_max_tokens - ), - ); - } let original_entries = self.session.history().entries(); let derived_sources = original_entries .iter() .map(|entry| entry.annotation.entry_id.clone()) .collect::>(); let generic_summary_input = session_store::SystemInputEnvelope::new( - new_history[0] + summary_message .as_text() .expect("compaction summary is text"), session_store::HistoryInputMetadata::new( @@ -7491,47 +7455,79 @@ impl Worker { }), generic_summary_input, ); - let mut original_cursor = 0usize; - let compacted_history_entries = new_history + let derived_entry = |item| { + let mut metadata = new_history_metadata( + WorkerHistoryProvenance::DerivedSummary, + Some(SessionHistoryDerivation { + sources: derived_sources.clone(), + }), + ); + metadata.input_metadata = Some( + session_store::HistoryInputMetadata::new( + session_store::HistoryInputUsePolicy { + recording_material: session_store::InputEvidenceUse::Exclude, + memory_evidence: session_store::InputEvidenceUse::Exclude, + observation: session_store::InputObservationUse::Hidden, + compaction: session_store::InputCompactionUse::Retain, + }, + None, + None, + ) + .expect("bounded private derived context policy"), + ); + HistoryEntry::new(item, metadata) + }; + // Retained items and annotations travel together, including when attachment + // projection changes an item. Equal model text never selects an identity. + let mut compacted_history_entries = + vec![HistoryEntry::new(summary_message, summary_entry.metadata)]; + compacted_history_entries.extend( + frozen_context.iter().map(|entry| { + HistoryEntry::new(entry.item.to_history_item(), entry.metadata.clone()) + }), + ); + compacted_history_entries.extend(auto_read_messages.into_iter().map(&derived_entry)); + if let Some(item) = reference_message { + compacted_history_entries.push(derived_entry(item)); + } + compacted_history_entries.extend(retained_history_entries); + compacted_history_entries.push(derived_entry(task_snapshot_message)); + compacted_history_entries.push(derived_entry(Item::tool_call( + "compact-tasklist", + "TaskList", + "{}", + ))); + compacted_history_entries.push(derived_entry(Item::tool_result_with_content( + "compact-tasklist", + self.task_feature.snapshot_overview(), + task_snapshot_text.clone(), + ))); + let new_history = compacted_history_entries .iter() - .cloned() - .enumerate() - .map(|(index, item)| { - if index == 0 { - HistoryEntry::new(item, summary_entry.metadata.clone()) - } else if let Some(context) = frozen_context.get(index - 1) { - HistoryEntry::new(item, context.metadata.clone()) - } else if let Some((offset, original)) = original_entries[original_cursor..] - .iter() - .enumerate() - .find(|(_, original)| original.item == item) - { - original_cursor += offset + 1; - HistoryEntry::new(item, original.annotation.clone()) - } else { - let mut metadata = new_history_metadata( - WorkerHistoryProvenance::DerivedSummary, - Some(SessionHistoryDerivation { - sources: derived_sources.clone(), - }), - ); - metadata.input_metadata = Some( - session_store::HistoryInputMetadata::new( - session_store::HistoryInputUsePolicy { - recording_material: session_store::InputEvidenceUse::Exclude, - memory_evidence: session_store::InputEvidenceUse::Exclude, - observation: session_store::InputObservationUse::Hidden, - compaction: session_store::InputCompactionUse::Retain, - }, - None, - None, - ) - .expect("bounded private derived context policy"), - ); - HistoryEntry::new(item, metadata) - } - }) + .map(|entry| entry.item.clone()) .collect::>(); + let result_estimate = agen::token_counter::total_tokens(&new_history, &[]); + if result_context_max_tokens > 0 && result_estimate.tokens > result_context_max_tokens { + // This history estimate includes retained items and auto-read files, not + // just the summary. Treat the configured result target as advisory; + // request-time context safety checks still apply after activation. + warn!( + result_tokens = result_estimate.tokens, + warning_threshold = result_context_max_tokens, + retained_tokens = retained_estimate.tokens, + summary_tokens, + auto_read_tokens, + "compacted result context exceeds warning threshold; continuing" + ); + self.alert( + AlertLevel::Warn, + AlertSource::Compactor, + format!( + "compacted result context is larger than expected (≈{} tokens; warning threshold {}); continuing compaction", + result_estimate.tokens, result_context_max_tokens + ), + ); + } let retained_user_segments = self .user_segments @@ -9238,13 +9234,14 @@ fn project_compactor_capture_attachments(entries: &mut [HistoryEntry]) -> /// Remove every durable binary attachment from the compacted tail and preserve /// re-readable ViewImage origins as related-file references. -fn project_compaction_attachments( - items: &mut [Item], +fn project_compaction_attachments<'a>( + items: impl IntoIterator, default_workdir_alias: Option<&str>, ) -> Vec { + let items = items.into_iter().collect::>(); let view_image_calls = items .iter() - .filter_map(|item| match item { + .filter_map(|item| match &**item { Item::ToolCall { call_id, name, @@ -10870,7 +10867,7 @@ mod build_summary_prompt_tests { Item::ToolResult { attachments, .. } if attachments.is_empty() )); - let references = project_compaction_attachments(&mut items, Some("main")); + let references = project_compaction_attachments(items.iter_mut(), Some("main")); assert_eq!( references, diff --git a/crates/worker/tests/compact_events_test.rs b/crates/worker/tests/compact_events_test.rs index c294f54aa..0869c0b5c 100644 --- a/crates/worker/tests/compact_events_test.rs +++ b/crates/worker/tests/compact_events_test.rs @@ -3655,3 +3655,132 @@ async fn journal_snapshot_counts_against_compactor_hard_input_cap_without_mutati ); } } +#[tokio::test] +async fn identical_reference_and_retained_inputs_preserve_distinct_metadata_after_compaction() { + use session_store::*; + let client = MockClient::new(vec![ + single_text_events("fixture"), + single_text_events("fork checkpoint"), + write_summary_tool_use_events("review-summary", "replacement summary"), + single_text_events("done"), + single_text_events("restore checkpoint"), + ]); + let mut fixture = SummaryInputFixture::new(MANUAL_ONLY_MANIFEST_TOML, client).await; + let make = |producer: &str, compaction| { + LoggedSystemHistoryEntry::from_input( + LoggedSessionHistoryOrigin::Provided { + producer: producer.into(), + }, + None, + SystemInputEnvelope::new( + "identical model context", + HistoryInputMetadata::new( + HistoryInputUsePolicy { + recording_material: InputEvidenceUse::Exclude, + memory_evidence: InputEvidenceUse::Exclude, + observation: InputObservationUse::Hidden, + compaction, + }, + Some( + FeatureInputPayload::encode( + "review:owner", + 1, + &serde_json::json!({"producer": producer}), + ) + .unwrap(), + ), + None, + ) + .unwrap(), + ) + .unwrap(), + ) + .to_history_entry() + }; + let historical = make("old-reference", InputCompactionUse::ReferenceInput); + let retained = make("retained-context", InputCompactionUse::Retain); + let repeated = make("second-retained-context", InputCompactionUse::Retain); + assert_ne!(historical.metadata.entry_id, retained.metadata.entry_id); + fixture + .restore_seed( + vec![historical.clone(), retained.clone(), repeated.clone()], + false, + ) + .await; + let session = fixture.worker.session_id(); + let original = fixture.worker.segment_id(); + fixture + .worker + .store() + .append( + session, + original, + &LogEntry::Extension { + ts: 2, + domain: "test.fork_drift".into(), + payload: serde_json::json!({}), + }, + ) + .unwrap(); + fixture.worker.run_text("fork checkpoint").await.unwrap(); + let fork = fixture.worker.segment_id(); + assert_ne!(fork, original); + let forked = replacement_seed(&fixture.worker); + for expected in [&historical, &retained, &repeated] { + assert!(forked.contains(expected)); + } + fixture.worker.compact(1_000_000).await.unwrap(); + let seed = replacement_seed(&fixture.worker); + let matching: Vec<_> = seed.iter().filter(|e| e.item == retained.item).collect(); + assert_eq!(matching.len(), 2); + assert_eq!( + matching[0].metadata, retained.metadata, + "a retained logical entry must not inherit identity/policy/payload from an equal-body reference that was removed" + ); + + assert_eq!(matching[1].metadata, repeated.metadata); + let compacted = fixture.worker.segment_id(); + let reopened = FsStore::new(fixture._store_dir.path()).unwrap(); + let records = reopened.read_all(session, compacted).unwrap(); + assert!( + matches!(&records[0], LogEntry::AnnotatedSegmentStart { compacted_from: Some(source), .. } if source.segment_id == fork) + ); + fixture.worker.run_text("restore checkpoint").await.unwrap(); + let records = fixture.worker.store().read_all(session, compacted).unwrap(); + let snapshot = public_snapshot::project_current_session_snapshot(&records); + let checkpoint = snapshot + .entries + .iter() + .rev() + .find(|entry| { + matches!( + entry.data, + protocol::SessionSnapshotEntryData::UserInput { .. } + ) + }) + .unwrap(); + fixture + .worker + .rewind_to(checkpoint.entry_id.clone()) + .await + .unwrap(); + for expected in [&retained, &repeated] { + assert!( + fixture + .worker + .session_history() + .entries() + .iter() + .any(|entry| entry.annotation == expected.metadata + && entry.item == Item::from(expected.item.clone())) + ); + } + assert!( + !fixture + .worker + .session_history() + .entries() + .iter() + .any(|entry| entry.annotation.entry_id == historical.metadata.entry_id) + ); +}