Expose app-server diagnostics through the experimental API (#37470)

## What changed

- Add the experimental `server/diagnostics` request, returning the app-server process ID, resident memory, platform-dependent physical footprint, and registered diagnostic gauges.
- Export the new protocol types in the generated JSON and TypeScript schemas and document the request.

## Testing

- Verify the response includes process measurements and the registered live-thread gauge.
- Verify the request is rejected unless `capabilities.experimentalApi` is enabled.

GitOrigin-RevId: 099d60a9c421a74ff1a72fe524e0dfeb89cf1057
This commit is contained in:
jif
2026-08-07 16:57:47 +00:00
committed by copyberry
parent b3278e96cb
commit 5729546839
18 changed files with 298 additions and 0 deletions
+1
View File
@@ -2009,6 +2009,7 @@ dependencies = [
"codex-connectors",
"codex-core",
"codex-core-plugins",
"codex-diagnostics",
"codex-exec-server",
"codex-extension-api",
"codex-external-agent-migration",
@@ -17739,6 +17739,52 @@
"title": "SendAddCreditsNudgeEmailResponse",
"type": "object"
},
"ServerDiagnosticsGauge": {
"properties": {
"name": {
"type": "string"
},
"value": {
"format": "uint64",
"minimum": 0.0,
"type": "integer"
}
},
"required": [
"name",
"value"
],
"type": "object"
},
"ServerDiagnosticsProcess": {
"properties": {
"id": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"physicalFootprintBytes": {
"format": "uint64",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"residentMemoryBytes": {
"format": "uint64",
"minimum": 0.0,
"type": [
"integer",
"null"
]
}
},
"required": [
"id"
],
"type": "object"
},
"ServerRequestResolvedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -14076,6 +14076,52 @@
"title": "SendAddCreditsNudgeEmailResponse",
"type": "object"
},
"ServerDiagnosticsGauge": {
"properties": {
"name": {
"type": "string"
},
"value": {
"format": "uint64",
"minimum": 0.0,
"type": "integer"
}
},
"required": [
"name",
"value"
],
"type": "object"
},
"ServerDiagnosticsProcess": {
"properties": {
"id": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"physicalFootprintBytes": {
"format": "uint64",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"residentMemoryBytes": {
"format": "uint64",
"minimum": 0.0,
"type": [
"integer",
"null"
]
}
},
"required": [
"id"
],
"type": "object"
},
"ServerNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Notification sent from the server to the client.",
@@ -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 ServerDiagnosticsGauge = { name: string, value: number, };
@@ -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 ServerDiagnosticsProcess = { id: number, residentMemoryBytes: number | null, physicalFootprintBytes: number | null, };
@@ -391,6 +391,8 @@ export type { ScheduledTaskWeekday } from "./ScheduledTaskWeekday";
export type { SelectedCapabilityRoot } from "./SelectedCapabilityRoot";
export type { SendAddCreditsNudgeEmailParams } from "./SendAddCreditsNudgeEmailParams";
export type { SendAddCreditsNudgeEmailResponse } from "./SendAddCreditsNudgeEmailResponse";
export type { ServerDiagnosticsGauge } from "./ServerDiagnosticsGauge";
export type { ServerDiagnosticsProcess } from "./ServerDiagnosticsProcess";
export type { ServerRequestResolvedNotification } from "./ServerRequestResolvedNotification";
export type { SessionMigration } from "./SessionMigration";
export type { SessionSource } from "./SessionSource";
@@ -478,6 +478,14 @@ client_request_definitions! {
response: v1::InitializeResponse,
},
#[experimental("server/diagnostics")]
/// Read content-free, process-local diagnostics.
ServerDiagnostics => "server/diagnostics" {
params: v2::ServerDiagnosticsParams,
serialization: None,
response: v2::ServerDiagnosticsResponse,
},
/// NEW APIs
// Thread lifecycle
// Uses `inspect_params` because only some fields are experimental.
@@ -0,0 +1,37 @@
use crate::JsonSchema;
use crate::TS;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ServerDiagnosticsParams {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ServerDiagnosticsResponse {
pub process: ServerDiagnosticsProcess,
pub gauges: Vec<ServerDiagnosticsGauge>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ServerDiagnosticsProcess {
pub id: u32,
#[ts(type = "number | null")]
pub resident_memory_bytes: Option<u64>,
#[ts(type = "number | null")]
pub physical_footprint_bytes: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ServerDiagnosticsGauge {
pub name: String,
#[ts(type = "number")]
pub value: u64,
}
@@ -7,6 +7,7 @@ mod collaboration_mode;
mod command_exec;
mod config;
mod current_time;
mod diagnostics;
mod environment;
mod experimental_feature;
mod feedback;
@@ -35,6 +36,7 @@ pub use collaboration_mode::*;
pub use command_exec::*;
pub use config::*;
pub use current_time::*;
pub use diagnostics::*;
pub use environment::*;
pub use experimental_feature::*;
pub use feedback::*;
+1
View File
@@ -42,6 +42,7 @@ codex-config = { workspace = true }
codex-connectors = { workspace = true }
codex-core = { workspace = true }
codex-core-plugins = { workspace = true }
codex-diagnostics = { workspace = true }
codex-home = { workspace = true }
codex-exec-server = { workspace = true }
codex-extension-api = { workspace = true }
+17
View File
@@ -160,6 +160,7 @@ Example with notification opt-out:
## API Overview
- `server/diagnostics` — experimental; read process-local memory measurements and registered diagnostic gauges.
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. Experimental `historyMode: "paginated"` selects projection-backed durable history. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `allowProviderModelFallback` lets providers backed by an authoritative static model catalog replace an unavailable requested `model` with the catalog default; dynamic or cached catalogs preserve the requested model. Experimental `runtimeWorkspaceRoots` supplies the runtime workspace roots used when app-server creates default environment selections; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd` and optional environment-native `runtimeWorkspaceRoots`. Explicit environments ignore the top-level roots; omitted per-environment roots default to that environment's `cwd`, while an empty list explicitly selects no roots. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are started in that environment, and HTTP MCP connections use that environment's HTTP client.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; pass an optional `lastTurnId` to copy history only through that turn, inclusive, and drop later turns from the fork. An in-progress `lastTurnId` boundary is rejected. Experimental `beforeTurnId` instead copies history strictly before the referenced turn, including when that turn is in progress, and cannot be combined with `lastTurnId`. If both boundaries are null while the source thread is mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately, or `deferGoalContinuation: true` to carry the source thread's current goal into the fork and run an explicit turn before automatic continuation resumes. Deferred goal continuation is persisted until that turn starts and cannot be combined with `ephemeral: true`. Accepts the same permission override rules as `thread/start`.
@@ -486,6 +487,22 @@ Enable `capabilities.experimentalApi` during initialization, then use `thread/li
} }
```
### Example: Read server diagnostics
`server/diagnostics` returns measurements for the app-server process and its registered gauges. Enable `capabilities.experimentalApi` during initialization. Physical footprint is available on macOS and is `null` on other platforms.
```json
{ "method": "server/diagnostics", "id": 22, "params": {} }
{ "id": 22, "result": {
"process": {
"id": 1234,
"residentMemoryBytes": 4194304,
"physicalFootprintBytes": 5242880
},
"gauges": [{ "name": "core.threads.live", "value": 1 }]
} }
```
### Example: Track thread status changes
`thread/status/changed` is emitted whenever a loaded thread's status changes after it has already been introduced to the client:
@@ -40,6 +40,7 @@ use crate::request_processors::ThreadGoalRequestProcessor;
use crate::request_processors::ThreadRequestProcessor;
use crate::request_processors::TurnRequestProcessor;
use crate::request_processors::WindowsSandboxRequestProcessor;
use crate::request_processors::read_server_diagnostics;
use crate::request_serialization::QueuedInitializedRequest;
use crate::request_serialization::RequestSerializationQueueKey;
use crate::request_serialization::RequestSerializationQueues;
@@ -900,6 +901,7 @@ impl MessageProcessor {
ClientRequest::Initialize { .. } => {
panic!("Initialize should be handled before initialized request dispatch");
}
ClientRequest::ServerDiagnostics { .. } => Ok(Some(read_server_diagnostics().into())),
ClientRequest::ConfigRead { params, .. } => self
.config_processor
.read(params)
@@ -526,6 +526,7 @@ mod bedrock_auth;
mod catalog_processor;
mod command_exec_processor;
mod config_processor;
mod diagnostics;
mod environment_processor;
mod feedback_doctor_report;
mod feedback_processor;
@@ -551,6 +552,7 @@ pub(crate) use apps_processor::AppsRequestProcessor;
pub(crate) use catalog_processor::CatalogRequestProcessor;
pub(crate) use command_exec_processor::CommandExecRequestProcessor;
pub(crate) use config_processor::ConfigRequestProcessor;
pub(crate) use diagnostics::read_server_diagnostics;
pub(crate) use environment_processor::EnvironmentRequestProcessor;
pub(crate) use feedback_processor::FeedbackRequestProcessor;
pub(crate) use fs_processor::FsRequestProcessor;
@@ -0,0 +1,23 @@
use codex_app_server_protocol::ServerDiagnosticsGauge;
use codex_app_server_protocol::ServerDiagnosticsProcess;
use codex_app_server_protocol::ServerDiagnosticsResponse;
pub(crate) fn read_server_diagnostics() -> ServerDiagnosticsResponse {
let diagnostics = codex_diagnostics::snapshot();
ServerDiagnosticsResponse {
process: ServerDiagnosticsProcess {
id: diagnostics.process.id,
resident_memory_bytes: diagnostics.process.resident_memory_bytes,
physical_footprint_bytes: diagnostics.process.physical_footprint_bytes,
},
gauges: diagnostics
.gauges
.into_iter()
.map(|gauge| ServerDiagnosticsGauge {
name: gauge.name.to_string(),
value: gauge.value,
})
.collect(),
}
}
@@ -74,6 +74,7 @@ mod safety_check_downgrade;
#[cfg(not(target_os = "windows"))]
mod selected_capability_stack;
mod selected_environment;
mod server_diagnostics;
#[cfg(not(target_os = "windows"))]
mod session_end;
mod skills_list;
@@ -0,0 +1,100 @@
use std::time::Duration;
use anyhow::Result;
use app_test_support::DEFAULT_CLIENT_NAME;
use app_test_support::MockResponsesConfig;
use app_test_support::TestAppServer;
use app_test_support::create_mock_responses_server_repeating_assistant;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::InitializeCapabilities;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerDiagnosticsGauge;
use codex_app_server_protocol::ServerDiagnosticsParams;
use codex_app_server_protocol::ServerDiagnosticsResponse;
use codex_app_server_protocol::ThreadStartParams;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::TempDir;
use tokio::time::timeout;
const READ_TIMEOUT: Duration = Duration::from_secs(20);
#[tokio::test]
async fn server_diagnostics_exposes_process_and_registered_thread_gauge() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
app_server
.start_thread(ThreadStartParams::default())
.await?;
let diagnostics: ServerDiagnosticsResponse = app_server
.request(|request_id| ClientRequest::ServerDiagnostics {
request_id,
params: ServerDiagnosticsParams::default(),
})
.await?;
assert!(diagnostics.process.id > 0);
assert!(diagnostics.process.resident_memory_bytes.is_some());
#[cfg(target_os = "macos")]
assert!(diagnostics.process.physical_footprint_bytes.is_some());
#[cfg(not(target_os = "macos"))]
assert_eq!(diagnostics.process.physical_footprint_bytes, None);
assert_eq!(
diagnostics.gauges,
vec![ServerDiagnosticsGauge {
name: "core.threads.live".to_string(),
value: 1,
}]
);
Ok(())
}
#[tokio::test]
async fn server_diagnostics_requires_experimental_capability() -> Result<()> {
let codex_home = TempDir::new()?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build()
.await?;
let initialization = app_server
.initialize_with_capabilities(
ClientInfo {
name: DEFAULT_CLIENT_NAME.to_string(),
title: None,
version: "0.1.0".to_string(),
},
Some(InitializeCapabilities {
experimental_api: false,
..Default::default()
}),
)
.await?;
assert!(matches!(initialization, JSONRPCMessage::Response(_)));
let request_id = app_server
.send_raw_request("server/diagnostics", Some(json!({})))
.await?;
let error = timeout(
READ_TIMEOUT,
app_server.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, -32600);
assert_eq!(
error.error.message,
"server/diagnostics requires experimentalApi capability"
);
assert_eq!(error.error.data, None);
Ok(())
}