From e551bbf6c57d7421a80d3dc805fb2aaba8f6f1de Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 12 May 2026 03:51:17 +0100 Subject: [PATCH] Add Epiphany view lens wrapper --- .../schema/json/ClientRequest.json | 8 ++ .../codex_app_server_protocol.schemas.json | 8 ++ .../codex_app_server_protocol.v2.schemas.json | 8 ++ .../typescript/v2/ThreadEpiphanyViewLens.ts | 5 ++ .../schema/typescript/v2/index.ts | 1 + .../src/protocol/common.rs | 67 ++++++++++++++ .../app-server-protocol/src/protocol/v2.rs | 35 ++++++++ .../app-server/src/codex_message_processor.rs | 89 +++++++++++++++++++ 8 files changed, 221 insertions(+) create mode 100644 vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyViewLens.ts diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/vendor/codex/codex-rs/app-server-protocol/schema/json/ClientRequest.json index c720c03a..199846e7 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/vendor/codex/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -4749,6 +4749,14 @@ }, "type": "object" }, + "ThreadEpiphanyViewLens": { + "enum": [ + "scene", + "jobs", + "pressure" + ], + "type": "string" + }, "ThreadForkParams": { "description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", "properties": { diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 0a5c39a8..7f6a2aab 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -17833,6 +17833,14 @@ }, "type": "object" }, + "ThreadEpiphanyViewLens": { + "enum": [ + "scene", + "jobs", + "pressure" + ], + "type": "string" + }, "ThreadForkParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index c9720510..08d5bef9 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -15720,6 +15720,14 @@ }, "type": "object" }, + "ThreadEpiphanyViewLens": { + "enum": [ + "scene", + "jobs", + "pressure" + ], + "type": "string" + }, "ThreadForkParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyViewLens.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyViewLens.ts new file mode 100644 index 00000000..e66dfb5a --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyViewLens.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadEpiphanyViewLens = "scene" | "jobs" | "pressure"; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 1447892c..e626bfaa 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -387,6 +387,7 @@ export type { ThreadEpiphanyStateUpdatedField } from "./ThreadEpiphanyStateUpdat export type { ThreadEpiphanyStateUpdatedNotification } from "./ThreadEpiphanyStateUpdatedNotification"; export type { ThreadEpiphanyStateUpdatedSource } from "./ThreadEpiphanyStateUpdatedSource"; export type { ThreadEpiphanyUpdatePatch } from "./ThreadEpiphanyUpdatePatch"; +export type { ThreadEpiphanyViewLens } from "./ThreadEpiphanyViewLens"; export type { ThreadForkParams } from "./ThreadForkParams"; export type { ThreadForkResponse } from "./ThreadForkResponse"; export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams"; diff --git a/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs b/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs index 1b72e379..57552d7b 100644 --- a/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs @@ -341,6 +341,11 @@ client_request_definitions! { params: v2::ThreadEpiphanySceneParams, response: v2::ThreadEpiphanySceneResponse, }, + #[experimental("thread/epiphany/view")] + ThreadEpiphanyView => "thread/epiphany/view" { + params: v2::ThreadEpiphanyViewParams, + response: v2::ThreadEpiphanyViewResponse, + }, #[experimental("thread/epiphany/jobs")] ThreadEpiphanyJobs => "thread/epiphany/jobs" { params: v2::ThreadEpiphanyJobsParams, @@ -1891,6 +1896,68 @@ mod tests { Ok(()) } + #[test] + fn serialize_thread_epiphany_view_response() -> Result<()> { + let request = ClientRequest::ThreadEpiphanyView { + request_id: RequestId::Integer(8), + params: v2::ThreadEpiphanyViewParams { + thread_id: "thr_123".to_string(), + lenses: vec![v2::ThreadEpiphanyViewLens::Pressure], + }, + }; + assert_eq!(request.method(), "thread/epiphany/view"); + + let response = ClientResponse::ThreadEpiphanyView { + request_id: RequestId::Integer(8), + response: v2::ThreadEpiphanyViewResponse { + thread_id: "thr_123".to_string(), + lenses: vec![v2::ThreadEpiphanyViewLens::Pressure], + scene: None, + jobs: Vec::new(), + pressure: Some(v2::ThreadEpiphanyPressure { + status: v2::ThreadEpiphanyPressureStatus::Ready, + level: v2::ThreadEpiphanyPressureLevel::Low, + basis: v2::ThreadEpiphanyPressureBasis::ModelContextWindow, + used_tokens: Some(10), + model_context_window: Some(100), + model_auto_compact_token_limit: Some(100), + remaining_tokens: Some(90), + ratio_per_mille: Some(100), + should_prepare_compaction: false, + note: "Context window usage is below the compaction prep threshold." + .to_string(), + }), + }, + }; + + assert_eq!(response.id(), &RequestId::Integer(8)); + assert_eq!(response.method(), "thread/epiphany/view"); + assert_eq!( + json!({ + "method": "thread/epiphany/view", + "id": 8, + "response": { + "threadId": "thr_123", + "lenses": ["pressure"], + "pressure": { + "status": "ready", + "level": "low", + "basis": "modelContextWindow", + "usedTokens": 10, + "modelContextWindow": 100, + "modelAutoCompactTokenLimit": 100, + "remainingTokens": 90, + "ratioPerMille": 100, + "shouldPrepareCompaction": false, + "note": "Context window usage is below the compaction prep threshold." + } + } + }), + serde_json::to_value(&response)?, + ); + Ok(()) + } + #[test] fn serialize_thread_epiphany_jobs_response() -> Result<()> { let response = ClientResponse::ThreadEpiphanyJobs { diff --git a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs index 0ec2119f..6bfa6839 100644 --- a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3940,6 +3940,41 @@ pub struct ThreadEpiphanySceneResponse { pub scene: ThreadEpiphanyScene, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ThreadEpiphanyViewLens { + Scene, + Jobs, + Pressure, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyViewParams { + pub thread_id: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub lenses: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyViewResponse { + pub thread_id: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub lenses: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub scene: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub jobs: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub pressure: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs index d5b96805..6a2f5304 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs @@ -266,6 +266,9 @@ use codex_app_server_protocol::ThreadEpiphanyStateUpdatedSource; use codex_app_server_protocol::ThreadEpiphanyUpdateParams; use codex_app_server_protocol::ThreadEpiphanyUpdatePatch; use codex_app_server_protocol::ThreadEpiphanyUpdateResponse; +use codex_app_server_protocol::ThreadEpiphanyViewLens; +use codex_app_server_protocol::ThreadEpiphanyViewParams; +use codex_app_server_protocol::ThreadEpiphanyViewResponse; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadIncrementElicitationParams; @@ -1120,6 +1123,10 @@ impl CodexMessageProcessor { self.thread_epiphany_scene(to_connection_request_id(request_id), params) .await; } + ClientRequest::ThreadEpiphanyView { request_id, params } => { + self.thread_epiphany_view(to_connection_request_id(request_id), params) + .await; + } ClientRequest::ThreadEpiphanyJobs { request_id, params } => { self.thread_epiphany_jobs(to_connection_request_id(request_id), params) .await; @@ -4290,6 +4297,88 @@ impl CodexMessageProcessor { self.outgoing.send_response(request_id, response).await; } + async fn thread_epiphany_view( + &self, + request_id: ConnectionRequestId, + params: ThreadEpiphanyViewParams, + ) { + let ThreadEpiphanyViewParams { thread_id, lenses } = params; + let lenses = if lenses.is_empty() { + vec![ + ThreadEpiphanyViewLens::Scene, + ThreadEpiphanyViewLens::Jobs, + ThreadEpiphanyViewLens::Pressure, + ] + } else { + lenses + }; + + let thread_uuid = match ThreadId::from_string(&thread_id) { + Ok(id) => id, + Err(err) => { + self.send_invalid_request_error(request_id, format!("invalid thread id: {err}")) + .await; + return; + } + }; + + let loaded_thread = self.thread_manager.get_thread(thread_uuid).await.ok(); + let loaded = loaded_thread.is_some(); + let thread = match self.read_thread_view(thread_uuid, false).await { + Ok(thread) => thread, + Err(ThreadReadViewError::InvalidRequest(message)) => { + self.send_invalid_request_error(request_id, message).await; + return; + } + Err(ThreadReadViewError::Internal(message)) => { + self.send_internal_error(request_id, message).await; + return; + } + }; + + let retrieval_override = if lenses.contains(&ThreadEpiphanyViewLens::Jobs) + && thread + .epiphany_state + .as_ref() + .and_then(|state| state.retrieval.as_ref()) + .is_none() + { + if let Some(loaded_thread) = loaded_thread.as_ref() { + Some(loaded_thread.epiphany_retrieval_state().await) + } else { + None + } + } else { + None + }; + let token_usage_info = if lenses.contains(&ThreadEpiphanyViewLens::Pressure) { + if let Some(loaded_thread) = loaded_thread.as_ref() { + loaded_thread.token_usage_info().await + } else { + None + } + } else { + None + }; + + let response = ThreadEpiphanyViewResponse { + thread_id, + scene: lenses + .contains(&ThreadEpiphanyViewLens::Scene) + .then(|| map_epiphany_scene(thread.epiphany_state.as_ref(), loaded)), + jobs: if lenses.contains(&ThreadEpiphanyViewLens::Jobs) { + map_epiphany_jobs(thread.epiphany_state.as_ref(), retrieval_override.as_ref()) + } else { + Vec::new() + }, + pressure: lenses + .contains(&ThreadEpiphanyViewLens::Pressure) + .then(|| map_epiphany_pressure(token_usage_info.as_ref())), + lenses, + }; + self.outgoing.send_response(request_id, response).await; + } + async fn thread_epiphany_jobs( &self, request_id: ConnectionRequestId,