diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index de10d47073..33ef6e2272 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4252,6 +4252,7 @@ version = "0.0.0" dependencies = [ "chrono", "codex-app-server-protocol", + "codex-extension-items", "codex-git-utils", "codex-install-context", "codex-otel", diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 26f2dca514..22cc870e96 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -4787,6 +4787,7 @@ async fn turn_event_counts_completed_tool_items() { revised_prompt: None, result: "ok".to_string(), transparent_background: None, + failure: None, saved_path: None, }), ]; diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index e8198c8d6e..778086152a 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2286,6 +2286,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "ItemCompletedNotification": { "properties": { "completedAtMs": { @@ -4706,6 +4737,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 6fe008b322..b139c3194f 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12159,6 +12159,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "InputModality": { "description": "Canonical user-input modality tags advertised by a model.", "oneOf": [ @@ -19950,6 +19981,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index feee48c70c..a1dfc5fd89 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -8435,6 +8435,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "InitializeCapabilities": { "description": "Client-declared capabilities negotiated during initialize.", "properties": { @@ -17709,6 +17740,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index a69732f48b..7a3645e996 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -314,6 +314,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1216,6 +1247,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index 862d703323..63f8d48320 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -314,6 +314,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1216,6 +1247,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 7b7b735acd..8f2b83b537 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -452,6 +452,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1361,6 +1392,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 300985b7f1..8d3f71feea 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -556,6 +556,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1910,6 +1941,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 0d53fc4b0b..ed476d2c2a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -478,6 +478,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1701,6 +1732,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index 33ba54078f..3d66bfef44 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -478,6 +478,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1701,6 +1732,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 2ec7369931..13cf57b6ab 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -478,6 +478,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1701,6 +1732,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 46884f4f1e..6fc3dbbc12 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -556,6 +556,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1910,6 +1941,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index a390f5d370..3ecebc79c4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -478,6 +478,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1701,6 +1732,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index a99941bdd3..4b07755c40 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -556,6 +556,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1910,6 +1941,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index a545a28025..3132907fef 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -478,6 +478,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1701,6 +1732,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index e51e8d3ef5..0a14a81681 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -478,6 +478,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1701,6 +1732,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 9f462daf5d..40dc021878 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -452,6 +452,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1361,6 +1392,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 93decd9d8a..4e4505228a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -452,6 +452,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1361,6 +1392,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 1cab035f7b..927306c05f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -452,6 +452,37 @@ ], "type": "string" }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, "LegacyAppPathString": { "type": "string" }, @@ -1361,6 +1392,17 @@ }, { "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index f18db7e8e6..6fa530c30c 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 96613cfe3d..76b4bceb10 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/ImageGenerationFailure.ts b/codex-rs/app-server-protocol/schema/typescript/ImageGenerationFailure.ts new file mode 100644 index 0000000000..00cd17db2d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/ImageGenerationFailure.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 ImageGenerationFailure = { "type": "usageLimitExceeded", limitId: string, resetsAt: number | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ImageGenerationItem.ts b/codex-rs/app-server-protocol/schema/typescript/ImageGenerationItem.ts index 78d98c8295..645d275e41 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ImageGenerationItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ImageGenerationItem.ts @@ -2,5 +2,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "./AbsolutePathBuf"; +import type { ImageGenerationFailure } from "./ImageGenerationFailure"; -export type ImageGenerationItem = { id: string, status: string, revisedPrompt: string | null, result: string, transparentBackground?: boolean, savedPath?: AbsolutePathBuf, }; +export type ImageGenerationItem = { id: string, status: string, revisedPrompt: string | null, result: string, transparentBackground?: boolean, failure: ImageGenerationFailure | null, savedPath?: AbsolutePathBuf, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 8080f942ad..893117f759 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -37,6 +37,7 @@ export type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams"; export type { GitDiffToRemoteResponse } from "./GitDiffToRemoteResponse"; export type { GitSha } from "./GitSha"; export type { ImageDetail } from "./ImageDetail"; +export type { ImageGenerationFailure } from "./ImageGenerationFailure"; export type { ImageGenerationItem } from "./ImageGenerationItem"; export type { InitializeCapabilities } from "./InitializeCapabilities"; export type { InitializeParams } from "./InitializeParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 839427d6bf..c238faf44a 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -863,6 +863,7 @@ impl ThreadHistoryBuilder { revised_prompt: None, result: String::new(), transparent_background: None, + failure: None, saved_path: None, }); self.upsert_item_in_current_turn(item); @@ -875,6 +876,7 @@ impl ThreadHistoryBuilder { revised_prompt: payload.revised_prompt.clone(), result: payload.result.clone(), transparent_background: payload.transparent_background, + failure: payload.failure.clone(), saved_path: payload.saved_path.clone(), }); self.upsert_item_in_current_turn(item); @@ -2079,6 +2081,7 @@ mod tests { revised_prompt: Some("A blue square".to_string()), result: "cG5n".to_string(), transparent_background: Some(true), + failure: None, saved_path: Some(saved_path.clone()), }, )), @@ -2110,6 +2113,7 @@ mod tests { revised_prompt: Some("A blue square".to_string()), result: "cG5n".to_string(), transparent_background: Some(true), + failure: None, saved_path: Some(saved_path), })] ); @@ -2373,6 +2377,12 @@ mod tests { revised_prompt: Some("final prompt".into()), result: "Zm9v".into(), transparent_background: Some(true), + failure: Some( + codex_extension_items::image_generation::ImageGenerationFailure::UsageLimitExceeded { + limit_id: "image_gen".into(), + resets_at: Some(1_786_150_800), + }, + ), saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()), })), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { @@ -2413,6 +2423,12 @@ mod tests { revised_prompt: Some("final prompt".into()), result: "Zm9v".into(), transparent_background: Some(true), + failure: Some( + codex_extension_items::image_generation::ImageGenerationFailure::UsageLimitExceeded { + limit_id: "image_gen".into(), + resets_at: Some(1_786_150_800), + }, + ), saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()), }), ], diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index 25ee56dead..5c9afedde0 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -15,6 +15,7 @@ use crate::protocol::item_builders::convert_patch_changes; use crate::protocol::item_builders::review_output_text; use codex_experimental_api_macros::ExperimentalApi; use codex_extension_items::ExtensionItem; +pub use codex_extension_items::image_generation::ImageGenerationFailure; pub use codex_extension_items::image_generation::ImageGenerationItem; pub use codex_extension_items::sleep::SleepItem; pub use codex_extension_items::web_search::WebSearchAction; @@ -929,6 +930,7 @@ impl From for ThreadItem { revised_prompt: image.revised_prompt, result: image.result, transparent_background: None, + failure: None, saved_path: image.saved_path, }) } diff --git a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs index 8da7582116..ea56169364 100644 --- a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs +++ b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs @@ -107,6 +107,7 @@ mod tests { revised_prompt: Some("revised".to_string()), result: "base64-result".to_string(), transparent_background: None, + failure: None, saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()), }), ]); diff --git a/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs index 7130fdc19a..51e5f9c567 100644 --- a/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs +++ b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs @@ -7,6 +7,7 @@ use app_test_support::ChatGptAuthFixture; use app_test_support::MockResponsesConfig; use app_test_support::TestAppServer; use app_test_support::write_chatgpt_auth; +use codex_app_server_protocol::ImageGenerationFailure; use codex_app_server_protocol::ImageGenerationItem; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ThreadItem; @@ -416,6 +417,7 @@ async fn standalone_image_generation_failure_emits_terminal_item() -> Result<()> revised_prompt: Some("paint a blue whale".to_string()), result: String::new(), transparent_background: None, + failure: None, saved_path: None, }) ); @@ -430,10 +432,145 @@ async fn standalone_image_generation_failure_emits_terminal_item() -> Result<()> let (output, _) = requests[1] .function_call_output_content_and_success(call_id) .context("image generation function output should be present")?; - assert!( - output - .as_deref() - .is_some_and(|text| text.contains("image generation failed")) + assert_eq!( + output.as_deref(), + Some( + "image generation failed: http 500 Internal Server Error: Some(\"image backend failed\")" + ) + ); + + Ok(()) +} + +#[tokio::test] +async fn image_generation_usage_limit_preserves_correlated_failure_metadata() -> Result<()> { + let call_id = "image-run-limited"; + let reset_at = 1_786_150_800; + let server = responses::start_mock_server().await; + Mock::given(method("POST")) + .and(path("/api/codex/images/generations")) + .respond_with( + ResponseTemplate::new(429) + .insert_header("x-codex-active-limit", "image_gen") + .insert_header("x-image-gen-primary-used-percent", "100") + .insert_header("x-image-gen-primary-window-minutes", "1440") + .insert_header("x-image-gen-primary-reset-at", reset_at.to_string()) + .set_body_json(json!({ + "error": { + "type": "usage_limit_reached", + "message": "image limit reached", + "resets_at": reset_at, + "plan_type": "plus" + } + })), + ) + .expect(1) + .mount(&server) + .await; + responses::mount_sse_sequence( + &server, + vec![ + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_function_call_with_namespace( + call_id, + "image_gen", + "imagegen", + &json!({"prompt": "paint a blue whale"}).to_string(), + ), + responses::ev_completed("resp-1"), + ]), + responses::sse(vec![ + responses::ev_assistant_message("msg-1", "The image limit was reached."), + responses::ev_completed("resp-2"), + ]), + ], + ) + .await; + + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri(), ImagegenTestMode::Direct)?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("access-chatgpt"), + AuthCredentialsStoreMode::File, + )?; + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .with_env_overrides(&[("OPENAI_API_KEY", None)]) + .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) + .await?; + start_image_generation_turn(&mut mcp, ThreadStartParams::default()).await?; + + let completed = timeout( + DEFAULT_READ_TIMEOUT, + wait_for_image_generation_completed(&mut mcp), + ) + .await??; + let thread_id = completed.thread_id.clone(); + let ThreadItem::ImageGeneration(image) = completed.item else { + panic!("expected failed image-generation item"); + }; + assert_eq!(image.status, "failed"); + assert_eq!( + image.failure, + Some(ImageGenerationFailure::UsageLimitExceeded { + limit_id: "image_gen".to_string(), + resets_at: Some(reset_at), + }) + ); + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + let read_id = mcp + .send_thread_read_request(ThreadReadParams { + thread_id: thread_id.clone(), + include_turns: true, + }) + .await?; + let ThreadReadResponse { thread, .. } = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??; + let persisted_failure = thread + .turns + .iter() + .flat_map(|turn| turn.items.iter()) + .find_map(|item| match item { + ThreadItem::ImageGeneration(item) => item.failure.as_ref(), + _ => None, + }); + assert_eq!( + persisted_failure, + Some(&ImageGenerationFailure::UsageLimitExceeded { + limit_id: "image_gen".to_string(), + resets_at: Some(reset_at), + }) + ); + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id, + ..Default::default() + }) + .await?; + let ThreadResumeResponse { thread, .. } = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(resume_id)).await??; + let resumed_failure = thread + .turns + .iter() + .flat_map(|turn| turn.items.iter()) + .find_map(|item| match item { + ThreadItem::ImageGeneration(item) => item.failure.as_ref(), + _ => None, + }); + assert_eq!( + resumed_failure, + Some(&ImageGenerationFailure::UsageLimitExceeded { + limit_id: "image_gen".to_string(), + resets_at: Some(reset_at), + }) ); Ok(()) diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index fa18a5e3be..62f9780d59 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -1844,6 +1844,7 @@ fn append_resume_redaction_history( revised_prompt: Some("secret revised prompt".to_string()), result: "base64-image-result".to_string(), transparent_background: None, + failure: None, saved_path: Some(test_absolute_path("/tmp/ig-1.png")), }), ] diff --git a/codex-rs/core/src/tools/handlers/extension_tools.rs b/codex-rs/core/src/tools/handlers/extension_tools.rs index 8b4b9db02c..eae4622576 100644 --- a/codex-rs/core/src/tools/handlers/extension_tools.rs +++ b/codex-rs/core/src/tools/handlers/extension_tools.rs @@ -473,6 +473,7 @@ mod tests { revised_prompt: None, result: String::new(), transparent_background: None, + failure: None, saved_path: None, }); let expected_completed_item = ExtensionItem::ImageGeneration(ImageGenerationItem { @@ -481,6 +482,7 @@ mod tests { revised_prompt: Some("A tiny blue square".to_string()), result: "cG5n".to_string(), transparent_background: Some(true), + failure: None, saved_path: Some(expected_path.clone()), }); codex_tools::TurnItemEmitter::emit_started( @@ -503,6 +505,7 @@ mod tests { revised_prompt: Some("A tiny blue square".to_string()), result: "cG5n".to_string(), transparent_background: Some(true), + failure: None, saved_path: Some(expected_path.clone()), })], }, diff --git a/codex-rs/ext/image-generation/src/backend.rs b/codex-rs/ext/image-generation/src/backend.rs index 734d9becb0..beb8b99556 100644 --- a/codex-rs/ext/image-generation/src/backend.rs +++ b/codex-rs/ext/image-generation/src/backend.rs @@ -3,14 +3,46 @@ use codex_api::ImageGenerationRequest; use codex_api::ImageResponse; use codex_api::ImagesClient; use codex_api::ReqwestTransport; +use codex_api::map_api_error; use codex_login::default_client::add_originator_header; use codex_login::default_client::create_client; use codex_model_provider::SharedModelProvider; +use codex_protocol::error::CodexErr; use http::HeaderMap; use http::HeaderValue; const X_CODEX_IMAGE_TURN_ID_HEADER: &str = "x-codex-image-turn-id"; +pub(crate) struct ImageBackendError { + message: String, + codex_error: CodexErr, +} + +impl ImageBackendError { + fn from_api(error: codex_api::ApiError) -> Self { + let message = error.to_string(); + Self { + message, + codex_error: map_api_error(error), + } + } + + fn from_message(message: String) -> Self { + Self { + codex_error: CodexErr::Stream(message.clone()), + message, + } + } + + pub(crate) fn message(&self) -> &str { + &self.message + } + + pub(crate) fn codex_error(&self) -> &CodexErr { + &self.codex_error + } +} + #[derive(Clone)] pub(crate) struct CodexImagesBackend { provider: SharedModelProvider, @@ -27,17 +59,17 @@ impl CodexImagesBackend { } /// Resolves the provider and auth required for the current image API request. - async fn client(&self) -> Result, String> { + async fn client(&self) -> Result, ImageBackendError> { let provider = self .provider .api_provider() .await - .map_err(|err| err.to_string())?; + .map_err(|err| ImageBackendError::from_message(err.to_string()))?; let auth = self .provider .api_auth() .await - .map_err(|err| err.to_string())?; + .map_err(|err| ImageBackendError::from_message(err.to_string()))?; Ok(ImagesClient::new( ReqwestTransport::from_http_client(create_client()), provider, @@ -50,7 +82,7 @@ impl CodexImagesBackend { &self, request: ImageGenerationRequest, turn_id: &str, - ) -> Result { + ) -> Result { self.client() .await? .generate( @@ -58,7 +90,7 @@ impl CodexImagesBackend { image_request_headers(self.originator.as_deref(), turn_id), ) .await - .map_err(|err| err.to_string()) + .map_err(ImageBackendError::from_api) } /// Sends a standalone image edit request through the configured Images client. @@ -66,7 +98,7 @@ impl CodexImagesBackend { &self, request: ImageEditRequest, turn_id: &str, - ) -> Result { + ) -> Result { self.client() .await? .edit( @@ -74,7 +106,7 @@ impl CodexImagesBackend { image_request_headers(self.originator.as_deref(), turn_id), ) .await - .map_err(|err| err.to_string()) + .map_err(ImageBackendError::from_api) } } diff --git a/codex-rs/ext/image-generation/src/tool.rs b/codex-rs/ext/image-generation/src/tool.rs index 3483e15a1b..ab973da74c 100644 --- a/codex-rs/ext/image-generation/src/tool.rs +++ b/codex-rs/ext/image-generation/src/tool.rs @@ -22,7 +22,10 @@ use codex_extension_api::ToolPayload; use codex_extension_api::ToolSpec; use codex_extension_api::parse_tool_input_schema; use codex_extension_items::ExtensionItem; +use codex_extension_items::image_generation::ImageGenerationFailure; use codex_extension_items::image_generation::ImageGenerationItem; +use codex_protocol::error::CodexErr; +use codex_protocol::error::CodexErrorDetails; use codex_protocol::models::ContentItem; use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputBody; @@ -97,6 +100,7 @@ fn legacy_end_event(item: &ImageGenerationItem) -> EventMsg { revised_prompt: item.revised_prompt.clone(), result: item.result.clone(), transparent_background: item.transparent_background, + failure: item.failure.clone(), saved_path: item.saved_path.clone(), }) } @@ -144,6 +148,7 @@ impl ImageGenerationTool { revised_prompt: None, result: String::new(), transparent_background: None, + failure: None, saved_path: None, }, EventMsg::ImageGenerationBegin(ImageGenerationBeginEvent { @@ -155,7 +160,12 @@ impl ImageGenerationTool { ImageRequest::Generate(request) => self.backend.generate(request, &call.turn_id).await, ImageRequest::Edit(request) => self.backend.edit(request, &call.turn_id).await, } - .map_err(|err| format!("image generation failed: {err}")) + .map_err(|error| { + ( + format!("image generation failed: {}", error.message()), + usage_limit_failure(error.codex_error()), + ) + }) .and_then(|response| { let transparent_background = match response.background { Some(ImageBackground::Transparent) => Some(true), @@ -167,17 +177,18 @@ impl ImageGenerationTool { .into_iter() .next() .map(|data| (data.b64_json, transparent_background)) - .ok_or_else(|| "image generation returned no image data".to_string()) + .ok_or_else(|| ("image generation returned no image data".to_string(), None)) }); let (result, transparent_background) = match result { Ok(result) => result, - Err(message) => { + Err((message, failure)) => { let item = ImageGenerationItem { id: call.call_id.clone(), status: "failed".to_string(), revised_prompt: Some(args.prompt), result: String::new(), transparent_background: None, + failure, saved_path: None, }; let legacy_event = legacy_end_event(&item); @@ -218,6 +229,7 @@ impl ImageGenerationTool { revised_prompt: Some(args.prompt), result: result.clone(), transparent_background, + failure: None, saved_path: saved_path.clone(), }; let legacy_event = legacy_end_event(&item); @@ -235,6 +247,33 @@ impl ImageGenerationTool { } } +fn usage_limit_failure(error: &CodexErr) -> Option { + let CodexErrorDetails::UsageLimitReached(usage_limit) = error.details() else { + return None; + }; + let rate_limits = usage_limit.rate_limits.as_deref()?; + let limit_id = rate_limits.limit_id.as_deref()?; + if limit_id != "image_gen" { + return None; + } + + let resets_at = if let Some(reset_at) = usage_limit.resets_at.as_ref() { + Some(reset_at.timestamp()) + } else { + [rate_limits.primary.as_ref(), rate_limits.secondary.as_ref()] + .into_iter() + .flatten() + .filter(|window| window.used_percent >= 100.0) + .filter_map(|window| window.resets_at) + .max() + }; + + Some(ImageGenerationFailure::UsageLimitExceeded { + limit_id: limit_id.to_string(), + resets_at, + }) +} + async fn save_image_generation_result( fs: &dyn ExecutorFileSystem, save_root: &AbsolutePathBuf, diff --git a/codex-rs/ext/items/src/image_generation.rs b/codex-rs/ext/items/src/image_generation.rs index 08a068c779..b7e6f15e58 100644 --- a/codex-rs/ext/items/src/image_generation.rs +++ b/codex-rs/ext/items/src/image_generation.rs @@ -4,6 +4,21 @@ use serde::Deserialize; use serde::Serialize; use ts_rs::TS; +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type", rename_all = "camelCase")] +pub enum ImageGenerationFailure { + UsageLimitExceeded { + #[serde(rename = "limitId")] + #[ts(rename = "limitId")] + limit_id: String, + #[serde(rename = "resetsAt")] + #[ts(rename = "resetsAt")] + #[ts(type = "number | null")] + resets_at: Option, + }, +} + // Standalone image-generation item owned by the image extension. This is also // the field-level representation exposed by app-server; core and rollout // persistence only carry it inside an ExtensionItem envelope. @@ -18,6 +33,8 @@ pub struct ImageGenerationItem { #[serde(default)] #[ts(optional)] pub transparent_background: Option, + #[serde(default)] + pub failure: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub saved_path: Option, diff --git a/codex-rs/ext/items/src/tests.rs b/codex-rs/ext/items/src/tests.rs index 32be56580c..00764e4d14 100644 --- a/codex-rs/ext/items/src/tests.rs +++ b/codex-rs/ext/items/src/tests.rs @@ -15,6 +15,7 @@ fn completed_image_generation_item() -> ExtensionItem { revised_prompt: Some("A blue square".to_string()), result: "cG5n".to_string(), transparent_background: None, + failure: None, saved_path: None, }) } @@ -33,6 +34,7 @@ fn image_generation_item_preserves_stable_wire_shape() { "revisedPrompt": "A blue square", "result": "cG5n", "transparentBackground": null, + "failure": null, }) ); assert_eq!( @@ -70,6 +72,7 @@ fn image_generation_item_preserves_authoritative_transparency() { "revisedPrompt": "A blue square", "result": "cG5n", "transparentBackground": true, + "failure": null, }) ); assert_eq!( diff --git a/codex-rs/protocol/src/legacy_events.rs b/codex-rs/protocol/src/legacy_events.rs index 225b8bd484..2cd66b8d78 100644 --- a/codex-rs/protocol/src/legacy_events.rs +++ b/codex-rs/protocol/src/legacy_events.rs @@ -430,6 +430,7 @@ impl ImageGenerationItem { revised_prompt: self.revised_prompt.clone(), result: self.result.clone(), transparent_background: None, + failure: None, saved_path: self.saved_path.clone(), }) } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index a16738aa61..ab2afef9df 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -55,6 +55,7 @@ use crate::request_permissions::RequestPermissionsEvent; use crate::request_permissions::RequestPermissionsResponse; use crate::request_user_input::RequestUserInputResponse; use crate::user_input::UserInput; +use codex_extension_items::image_generation::ImageGenerationFailure; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; use schemars::JsonSchema; @@ -2549,6 +2550,9 @@ pub struct ImageGenerationEndEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub transparent_background: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub failure: Option, #[serde(skip_serializing_if = "Option::is_none")] #[ts(optional)] pub saved_path: Option, diff --git a/codex-rs/thread-store/Cargo.toml b/codex-rs/thread-store/Cargo.toml index a30cecdc30..b0d2b15568 100644 --- a/codex-rs/thread-store/Cargo.toml +++ b/codex-rs/thread-store/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] chrono = { workspace = true, features = ["serde"] } codex-app-server-protocol = { workspace = true } +codex-extension-items = { workspace = true } codex-git-utils = { workspace = true } codex-install-context = { workspace = true } codex-otel = { workspace = true } diff --git a/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs b/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs index 561f679df4..35358abaeb 100644 --- a/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs +++ b/codex-rs/thread-store/src/local/rollout_migration/legacy_event.rs @@ -8,6 +8,8 @@ //! It intentionally stays separate from live thread-history reduction because migration needs a //! frozen adapter for historical rollout payloads. +use codex_extension_items::ExtensionItem; +use codex_extension_items::image_generation::ImageGenerationItem; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; use codex_protocol::items::CommandExecutionItem; @@ -17,7 +19,6 @@ use codex_protocol::items::DynamicToolCallStatus; use codex_protocol::items::EnteredReviewModeItem; use codex_protocol::items::ExitedReviewModeItem; use codex_protocol::items::FileChangeItem; -use codex_protocol::items::ImageGenerationItem; use codex_protocol::items::McpToolCallError; use codex_protocol::items::McpToolCallItem; use codex_protocol::items::McpToolCallStatus; @@ -156,13 +157,15 @@ pub(super) fn completed_item( None, )), EventMsg::ImageGenerationEnd(event) => Some(( - TurnItem::ImageGeneration(ImageGenerationItem { + TurnItem::Extension(ExtensionItem::ImageGeneration(ImageGenerationItem { id: event.call_id.clone(), status: event.status.clone(), revised_prompt: event.revised_prompt.clone(), result: event.result.clone(), + transparent_background: event.transparent_background, + failure: event.failure.clone(), saved_path: event.saved_path.clone(), - }), + })), None, )), EventMsg::ContextCompacted(_) => Some(( diff --git a/codex-rs/thread-store/src/local/rollout_migration_tests.rs b/codex-rs/thread-store/src/local/rollout_migration_tests.rs index f1c98f286f..ce4a9ea7af 100644 --- a/codex-rs/thread-store/src/local/rollout_migration_tests.rs +++ b/codex-rs/thread-store/src/local/rollout_migration_tests.rs @@ -7,6 +7,9 @@ use std::path::PathBuf; use std::os::unix::fs::PermissionsExt; use codex_app_server_protocol::build_turns_from_rollout_items; +use codex_extension_items::ExtensionItem; +use codex_extension_items::image_generation::ImageGenerationFailure; +use codex_extension_items::image_generation::ImageGenerationItem; use codex_protocol::AgentPath; use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary; @@ -17,6 +20,7 @@ use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AgentMessageEvent; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::ImageGenerationEndEvent; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::SandboxPolicy; @@ -397,6 +401,62 @@ async fn migration_projects_explicit_and_implicit_legacy_completed_items() { assert_eq!(reasoning["content"], json!(["raw"])); } +#[tokio::test] +async fn migration_preserves_image_generation_failure_metadata() { + let home = TempDir::new().expect("create Codex home"); + let thread_id = ThreadId::new(); + let expected_item = ImageGenerationItem { + id: "image-call".to_string(), + status: "failed".to_string(), + revised_prompt: Some("paint a blue whale".to_string()), + result: String::new(), + transparent_background: None, + failure: Some(ImageGenerationFailure::UsageLimitExceeded { + limit_id: "image_gen".to_string(), + resets_at: Some(1_786_150_800), + }), + saved_path: None, + }; + let image_completion = + RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(ImageGenerationEndEvent { + call_id: expected_item.id.clone(), + status: expected_item.status.clone(), + revised_prompt: expected_item.revised_prompt.clone(), + result: expected_item.result.clone(), + transparent_background: expected_item.transparent_background, + failure: expected_item.failure.clone(), + saved_path: expected_item.saved_path.clone(), + })); + let path = write_rollout( + home.path(), + thread_id, + SessionSource::Cli, + vec![ + started("image-turn"), + image_completion, + completed("image-turn"), + ], + ); + let store = indexed_store(home.path()).await; + + store + .migrate_rollouts(apply_options()) + .await + .expect("migrate legacy image completion"); + + let migrated_item = read_rollout(&path) + .into_iter() + .find_map(|line| match line.item { + RolloutItem::EventMsg(EventMsg::ItemCompleted(event)) => Some(event.item), + _ => None, + }) + .expect("migrated image completion"); + let TurnItem::Extension(ExtensionItem::ImageGeneration(migrated_item)) = migrated_item else { + panic!("expected migrated extension image-generation item"); + }; + assert_eq!(migrated_item, expected_item); +} + #[tokio::test] async fn migration_keeps_late_completions_in_their_original_turn() { let home = TempDir::new().expect("create Codex home"); diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index fbf04b2987..1663ff9643 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -713,6 +713,7 @@ pub(super) fn handle_image_generation_end( revised_prompt, result: String::new(), transparent_background: None, + failure: None, saved_path, }), }),