Timestamp app-server notifications at emission (#32905)

## What changed

- Add an optional `emittedAtMs` field to server notification envelopes and expose the envelope in the generated JSON and TypeScript schemas.
- Populate the Unix timestamp when app-server emits a notification, before transport routing, so every connection in a fan-out receives the same value.
- Keep the field optional for clients decoding notifications from older app-server versions.

## Testing

- Verify notification serialization includes `emittedAtMs`.
- Verify targeted fan-out reuses one timestamp across connections.

GitOrigin-RevId: b379cb11e8e91c558bcd3d46ce459311a6e81e54
This commit is contained in:
Owen Lin authored and copyberry committed 2026-07-14 00:38:04 +00:00
1 parent 7f3eb8a223
commit c39520f3d1
22 files changed
+400 -171

No files matched your search

@@ -3,7 +3,7 @@ use std::fmt;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::Result;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerNotificationEnvelope;
use codex_app_server_protocol::ServerRequest;
use serde::Serialize;
use tokio::sync::oneshot;
@@ -25,7 +25,7 @@ pub enum OutgoingMessage {
Request(ServerRequest),
/// AppServerNotification is specific to the case where this is run as an
/// "app server" as opposed to an MCP server.
AppServerNotification(ServerNotification),
AppServerNotification(ServerNotificationEnvelope),
Response(OutgoingResponse),
Error(OutgoingError),
}
@@ -281,6 +281,7 @@ mod tests {
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerNotificationEnvelope;
use pretty_assertions::assert_eq;
use serde_json::json;
use tokio::time::Duration;
@@ -443,14 +444,15 @@ mod tests {
writer_tx
.send(QueuedOutgoingMessage::new(
OutgoingMessage::AppServerNotification(ServerNotification::ConfigWarning(
ConfigWarningNotification {
OutgoingMessage::AppServerNotification(ServerNotificationEnvelope {
notification: ServerNotification::ConfigWarning(ConfigWarningNotification {
summary: "queued".to_string(),
details: None,
path: None,
range: None,
},
)),
}),
emitted_at_ms: Some(1_234),
}),
))
.await
.expect("writer queue should accept first message");
@@ -484,6 +486,7 @@ mod tests {
"summary": "queued",
"details": null,
},
"emittedAtMs": 1_234,
})
);
}
@@ -448,6 +448,7 @@ mod tests {
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerNotificationEnvelope;
use pretty_assertions::assert_eq;
use serde_json::json;
use tokio::time::timeout;
@@ -588,14 +589,15 @@ mod tests {
writer
.send(QueuedOutgoingMessage::new(
OutgoingMessage::AppServerNotification(ServerNotification::ConfigWarning(
ConfigWarningNotification {
OutgoingMessage::AppServerNotification(ServerNotificationEnvelope {
notification: ServerNotification::ConfigWarning(ConfigWarningNotification {
summary: "test".to_string(),
details: None,
path: None,
range: None,
},
)),
}),
emitted_at_ms: Some(1_234),
}),
))
.await
.expect("writer should accept queued message");
@@ -14,6 +14,7 @@ use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerNotificationEnvelope;
use pretty_assertions::assert_eq;
#[test]
@@ -74,12 +75,15 @@ fn splits_large_server_messages_into_wire_chunks() {
let envelope = ServerEnvelope {
event: ServerEvent::ServerMessage {
message: Box::new(OutgoingMessage::AppServerNotification(
ServerNotification::ConfigWarning(ConfigWarningNotification {
summary: "x".repeat(REMOTE_CONTROL_SEGMENT_MAX_BYTES),
details: None,
path: None,
range: None,
}),
ServerNotificationEnvelope {
notification: ServerNotification::ConfigWarning(ConfigWarningNotification {
summary: "x".repeat(REMOTE_CONTROL_SEGMENT_MAX_BYTES),
details: None,
path: None,
range: None,
}),
emitted_at_ms: Some(1_234),
},
)),
},
client_id: ClientId("client-1".to_string()),
@@ -25,6 +25,7 @@ use codex_app_server_protocol::RemoteControlPairingStartParams;
use codex_app_server_protocol::RemoteControlPairingStatusParams;
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerNotificationEnvelope;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::test_support::auth_manager_from_auth;
use codex_core::test_support::auth_manager_from_auth_with_home;
@@ -736,14 +737,15 @@ async fn remote_control_transport_manages_virtual_clients_and_routes_messages()
writer
.send(QueuedOutgoingMessage::new(
OutgoingMessage::AppServerNotification(ServerNotification::ConfigWarning(
ConfigWarningNotification {
OutgoingMessage::AppServerNotification(ServerNotificationEnvelope {
notification: ServerNotification::ConfigWarning(ConfigWarningNotification {
summary: "test".to_string(),
details: None,
path: None,
range: None,
},
)),
}),
emitted_at_ms: Some(1_234),
}),
))
.await
.expect("remote writer should accept outgoing message");
@@ -758,7 +760,8 @@ async fn remote_control_transport_manages_virtual_clients_and_routes_messages()
"params": {
"summary": "test",
"details": null,
}
},
"emittedAtMs": 1_234,
}
})
);
@@ -1344,14 +1347,15 @@ async fn remote_control_transport_clears_outgoing_buffer_when_backend_acks() {
writer
.send(QueuedOutgoingMessage::new(
OutgoingMessage::AppServerNotification(ServerNotification::ConfigWarning(
ConfigWarningNotification {
OutgoingMessage::AppServerNotification(ServerNotificationEnvelope {
notification: ServerNotification::ConfigWarning(ConfigWarningNotification {
summary: "stale".to_string(),
details: None,
path: None,
range: None,
},
)),
}),
emitted_at_ms: Some(1_234),
}),
))
.await
.expect("remote writer should accept outgoing message");
@@ -1367,7 +1371,8 @@ async fn remote_control_transport_clears_outgoing_buffer_when_backend_acks() {
"params": {
"summary": "stale",
"details": null,
}
},
"emittedAtMs": 1_234,
}
})
);
@@ -1634,14 +1639,15 @@ async fn remote_control_http_mode_enrolls_before_connecting() {
writer
.send(QueuedOutgoingMessage::new(
OutgoingMessage::AppServerNotification(ServerNotification::ConfigWarning(
ConfigWarningNotification {
OutgoingMessage::AppServerNotification(ServerNotificationEnvelope {
notification: ServerNotification::ConfigWarning(ConfigWarningNotification {
summary: "backend".to_string(),
details: None,
path: None,
range: None,
},
)),
}),
emitted_at_ms: Some(1_234),
}),
))
.await
.expect("remote writer should accept outgoing message");
@@ -1656,7 +1662,8 @@ async fn remote_control_http_mode_enrolls_before_connecting() {
"params": {
"summary": "backend",
"details": null,
}
},
"emittedAtMs": 1_234,
}
})
);
@@ -1825,6 +1825,7 @@ mod tests {
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerNotificationEnvelope;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::test_support::auth_manager_from_auth;
use codex_login::AuthDotJson;
@@ -3362,12 +3363,17 @@ mod tests {
ServerEnvelope {
event: ServerEvent::ServerMessage {
message: Box::new(OutgoingMessage::AppServerNotification(
ServerNotification::ConfigWarning(ConfigWarningNotification {
summary: summary.to_string(),
details: None,
path: None,
range: None,
}),
ServerNotificationEnvelope {
notification: ServerNotification::ConfigWarning(
ConfigWarningNotification {
summary: summary.to_string(),
details: None,
path: None,
range: None,
},
),
emitted_at_ms: Some(1_234),
},
)),
},
client_id: client_id.clone(),