Skip to content

Conversation

@owenlin0
Copy link
Contributor

@owenlin0 owenlin0 commented Nov 4, 2025

Typescript and JSON schema exports
While working on Thread/Turn/Items type definitions, I realize we will run into name conflicts between v1 and v2 APIs (e.g. RateLimitWindow which won't be reusable since v1 uses RateLimitWindow from protocol/ which uses snake_case, but we want to expose camelCase everywhere, so we'll define a V2 version of that struct that serializes as camelCase).

To set us up for a clean and isolated v2 API, generate types into a v2/ namespace for both typescript and JSON schema.

  • TypeScript: v2 types emit under out_dir/v2/*.ts, and root index.ts now re-exports them via export * as v2 from "./v2";.
  • JSON Schemas: v2 definitions bundle under #/definitions/v2/* rather than the root.

The location for the original types (v1 and types pulled from protocol/ and other core crates) haven't changed and are still at the root. This is for backwards compatibility: no breaking changes to existing usages of v1 APIs and types.

Notifications
While working on export.rs, I:

  • refactored server/client notifications with macros (like we already do for methods) so they also get exported (I noticed they weren't being exported at all).
  • removed the hardcoded list of types to export as JSON schema by leveraging the existing macros instead
  • and took a stab at API V2 notifications. These aren't wired up yet, and I expect to iterate on these this week.

@owenlin0 owenlin0 force-pushed the owen/feature/v2-notifications branch 2 times, most recently from fd76e2d to 4e55cd9 Compare November 4, 2025 20:04
@owenlin0 owenlin0 marked this pull request as ready for review November 4, 2025 20:14
@owenlin0 owenlin0 force-pushed the owen/feature/v2-notifications branch from 4e55cd9 to 30f8525 Compare November 4, 2025 20:16
@chatgpt-codex-connector
Copy link
Contributor

💡 Codex Review

// Merge all generated per-type JSON files (including the envelopes above)
// into a single definitions bundle. Also recognize a v2 namespace so that
// types like RateLimitSnapshot can co-exist between legacy and v2.
let json_files = json_files_in_recursive(out_dir)?;
for path in json_files {
// Skip the bundle we’re about to (re)generate.
if path.file_name().and_then(OsStr::to_str)
== Some("codex_app_server_protocol.schemas.json")
{
continue;
}
let file_stem = path
.file_stem()
.and_then(OsStr::to_str)
.ok_or_else(|| anyhow!(format!("Invalid schema file name {}", path.display())))?;
// Determine namespace and logical type name from either the stem
// (e.g., "v2::Type") or the directory layout (e.g., ".../v2/Type.json").
let (ns_opt, logical_name) = detect_namespace(&path, file_stem);
let mut schema_value: Value = serde_json::from_str(
&fs::read_to_string(&path)
.with_context(|| format!("Failed to read {}", path.display()))?,
)
.with_context(|| format!("Failed to parse JSON from {}", path.display()))?;
// Normalize; use the original stem as the base so existing naming rules apply.
annotate_schema(&mut schema_value, Some(file_stem));
if let Some(ref ns) = ns_opt {
// Rewrite internal $ref targets to point to the namespaced location
// under the bundle's definitions.
rewrite_refs_to_namespace(&mut schema_value, ns);
}
// Pull embedded definitions out and insert into our bundle map at the
// appropriate namespace.
if let Value::Object(ref mut obj) = schema_value
&& let Some(defs) = obj.remove("definitions")
&& let Value::Object(defs_obj) = defs
{
for (def_name, mut def_schema) in defs_obj {
if !SPECIAL_DEFINITIONS.contains(&def_name.as_str()) {
annotate_schema(&mut def_schema, Some(def_name.as_str()));
if let Some(ref ns) = ns_opt {
definitions
.entry(ns.to_string())
.or_insert_with(|| Value::Object(Map::new()));
if let Value::Object(defs_ns) = definitions
.get_mut(ns)
.ok_or_else(|| anyhow!("just inserted v2 namespace"))?
{
defs_ns.insert(def_name, def_schema);
}
} else {
definitions.insert(def_name, def_schema);
}

P1 Badge Prevent v2 schemas from overwriting v1 definitions

The bundling loop now reads every generated JSON schema and inserts any embedded definitions directly into the root map when ns_opt is None. ServerNotification.json (and other envelope files) still contain inline schemas for all of the v2 payload structs. Because these files live at the root, their embedded definitions (e.g. RateLimitSnapshot, RateLimitWindow) are inserted into the root #/definitions/* entries, overwriting the v1 versions you were trying to keep backward compatible. Consumers that previously dereferenced #/definitions/RateLimitSnapshot will now receive the camel‑case v2 shape. To preserve v1 compatibility, definitions coming from non‑namespaced files that describe v2 types need to be redirected into the v2 namespace (or skipped altogether) instead of being inserted at the root.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@owenlin0 owenlin0 force-pushed the owen/feature/v2-notifications branch from f7a80fa to d6160c3 Compare November 4, 2025 22:09
@owenlin0 owenlin0 force-pushed the owen/feature/v2-notifications branch 2 times, most recently from 2868a5a to 4506790 Compare November 4, 2025 22:42
@owenlin0 owenlin0 force-pushed the owen/feature/v2-notifications branch from 4506790 to 5f793a1 Compare November 4, 2025 22:48
pub enum ServerNotification {
server_notification_definitions! {
/// NEW NOTIFICATIONS
#[serde(rename = "thread/started")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chat claims the following is possible to eliminate the duplication:

macro_rules! notif_variant {
    ($name:ident, $wire:literal, $ty:path) => {
        #[serde(rename = $wire)]
        #[ts(rename = $wire)]
        #[strum(serialize = $wire)]
        $name($ty),
    };
}

server_notification_definitions! {
    /// NEW NOTIFICATIONS
    notif_variant!(ThreadStarted, "thread/started", v2::ThreadStartedNotification);
    notif_variant!(TurnStarted, "turn/started", v2::TurnStartedNotification);
    notif_variant!(TurnCompleted, "turn/completed", v2::TurnCompletedNotification);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe if $wire is "", then return only $name($ty)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading more, it seems you need a proc macro so you can do this:

#[multi_rename("thread/started")]
ThreadStarted(v2::ThreadStartedNotification),

via something like this:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, AttributeArgs, ItemEnum, NestedMeta, Lit};

#[proc_macro_attribute]
pub fn notify_variants(attr: TokenStream, item: TokenStream) -> TokenStream {
    // no attributes expected on the enum itself — disregard `attr`
    let mut input = parse_macro_input!(item as ItemEnum);

    for variant in &mut input.variants {
        let name = variant.ident.to_string();

        // Insert the 3 repeated attributes:
        variant.attrs.push(syn::parse_quote!(#[serde(rename = #name)]));
        variant.attrs.push(syn::parse_quote!(#[ts(rename = #name)]));
        variant.attrs.push(syn::parse_quote!(#[strum(serialize = #name)]));
    }

    TokenStream::from(quote! { #input })
}

Copy link
Contributor Author

@owenlin0 owenlin0 Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex came up with something cool!

server_notification_definitions! {
    /// NEW NOTIFICATIONS
    ThreadStarted => "thread/started" (v2::ThreadStartedNotification),
    TurnStarted => "turn/started" (v2::TurnStartedNotification),
    TurnCompleted => "turn/completed" (v2::TurnCompletedNotification),
    ItemStarted => "item/started" (v2::ItemStartedNotification),
    ItemCompleted => "item/completed" (v2::ItemCompletedNotification),
    AgentMessageDelta => "item/agentMessage/delta" (v2::AgentMessageDeltaNotification),
    CommandExecutionOutputDelta => "item/commandExecution/outputDelta" (v2::CommandExecutionOutputDeltaNotification),
    McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification),
    AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
    AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification),

    /// DEPRECATED NOTIFICATIONS below
    AuthStatusChange(v1::AuthStatusChangeNotification),
    LoginChatGptComplete(v1::LoginChatGptCompleteNotification),
    SessionConfigured(v1::SessionConfiguredNotification),
}

was able to do update the original macro to support this. what do you think?


/// DEPRECATED NOTIFICATIONS below
/// Authentication status changed
AuthStatusChange(v1::AuthStatusChangeNotification),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this why we have to do the triple #[] thing on the other variants? Should this be a separate enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah exactly, the same enum has v1 and v2 variants

@owenlin0 owenlin0 enabled auto-merge (squash) November 5, 2025 00:52
@owenlin0 owenlin0 merged commit edf4c3f into main Nov 5, 2025
25 checks passed
@owenlin0 owenlin0 deleted the owen/feature/v2-notifications branch November 5, 2025 01:02
@github-actions github-actions bot locked and limited conversation to collaborators Nov 5, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants