diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 641708d961..d2a9de926a 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -4941,25 +4941,6 @@ ], "type": "object" }, - "ThreadRollbackParams": { - "description": "DEPRECATED: `thread/rollback` will be removed soon.", - "properties": { - "numTurns": { - "description": "The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.", - "format": "uint32", - "minimum": 0.0, - "type": "integer" - }, - "threadId": { - "type": "string" - } - }, - "required": [ - "numTurns", - "threadId" - ], - "type": "object" - }, "ThreadSearchSortKey": { "enum": [ "created_at", @@ -6325,30 +6306,6 @@ "title": "Thread/approveGuardianDeniedActionRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "thread/rollback" - ], - "title": "Thread/rollbackRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRollbackParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Thread/rollbackRequest", - "type": "object" - }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 83129264a9..83aa38fdcf 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -4276,7 +4276,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, 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 6bbd4aedbd..9e791e93e3 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 @@ -684,30 +684,6 @@ "title": "Thread/approveGuardianDeniedActionRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/v2/RequestId" - }, - "method": { - "enum": [ - "thread/rollback" - ], - "title": "Thread/rollbackRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/ThreadRollbackParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Thread/rollbackRequest", - "type": "object" - }, { "properties": { "id": { @@ -20602,7 +20578,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/v2/Turn" }, @@ -23503,45 +23479,6 @@ "title": "ThreadRevertedNotification", "type": "object" }, - "ThreadRollbackParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "DEPRECATED: `thread/rollback` will be removed soon.", - "properties": { - "numTurns": { - "description": "The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.", - "format": "uint32", - "minimum": 0.0, - "type": "integer" - }, - "threadId": { - "type": "string" - } - }, - "required": [ - "numTurns", - "threadId" - ], - "title": "ThreadRollbackParams", - "type": "object" - }, - "ThreadRollbackResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "thread": { - "allOf": [ - { - "$ref": "#/definitions/v2/Thread" - } - ], - "description": "The updated thread after applying the rollback, with `turns` populated.\n\nThe ThreadItems stored in each Turn are lossy since we explicitly do not persist all agent interactions, such as command executions. This is the same behavior as `thread/resume`." - } - }, - "required": [ - "thread" - ], - "title": "ThreadRollbackResponse", - "type": "object" - }, "ThreadSearchResult": { "properties": { "snippet": { 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 45f5721082..483c1fb9da 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 @@ -2214,30 +2214,6 @@ "title": "Thread/approveGuardianDeniedActionRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "thread/rollback" - ], - "title": "Thread/rollbackRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/ThreadRollbackParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Thread/rollbackRequest", - "type": "object" - }, { "properties": { "id": { @@ -18316,7 +18292,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, @@ -21217,45 +21193,6 @@ "title": "ThreadRevertedNotification", "type": "object" }, - "ThreadRollbackParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "DEPRECATED: `thread/rollback` will be removed soon.", - "properties": { - "numTurns": { - "description": "The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.", - "format": "uint32", - "minimum": 0.0, - "type": "integer" - }, - "threadId": { - "type": "string" - } - }, - "required": [ - "numTurns", - "threadId" - ], - "title": "ThreadRollbackParams", - "type": "object" - }, - "ThreadRollbackResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "thread": { - "allOf": [ - { - "$ref": "#/definitions/Thread" - } - ], - "description": "The updated thread after applying the rollback, with `turns` populated.\n\nThe ThreadItems stored in each Turn are lossy since we explicitly do not persist all agent interactions, such as command executions. This is the same behavior as `thread/resume`." - } - }, - "required": [ - "thread" - ], - "title": "ThreadRollbackResponse", - "type": "object" - }, "ThreadSearchResult": { "properties": { "snippet": { 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 829579af3c..8a7fa76e53 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1456,7 +1456,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, 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 58a0e80aae..32101efd89 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1247,7 +1247,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, 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 1975d98e26..f411af6d60 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -1247,7 +1247,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, 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 b3e3ca9ba3..a639e719b8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1247,7 +1247,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, 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 9569af82c3..ba6a006381 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1456,7 +1456,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json index 72928d2f2a..8dd0da9b6a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRevertResponse.json @@ -1247,7 +1247,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackParams.json deleted file mode 100644 index aa52fbd598..0000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackParams.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "DEPRECATED: `thread/rollback` will be removed soon.", - "properties": { - "numTurns": { - "description": "The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.", - "format": "uint32", - "minimum": 0.0, - "type": "integer" - }, - "threadId": { - "type": "string" - } - }, - "required": [ - "numTurns", - "threadId" - ], - "title": "ThreadRollbackParams", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json deleted file mode 100644 index 33396542a5..0000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ /dev/null @@ -1,2736 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "AbsolutePathBuf": { - "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - "type": "string" - }, - "AgentMessageDelivery": { - "enum": [ - "async" - ], - "type": "string" - }, - "AgentPath": { - "type": "string" - }, - "AsyncUserInputQuestion": { - "additionalProperties": false, - "properties": { - "options": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "title": { - "type": "string" - } - }, - "required": [ - "title" - ], - "type": "object" - }, - "ByteRange": { - "properties": { - "end": { - "format": "uint", - "minimum": 0.0, - "type": "integer" - }, - "start": { - "format": "uint", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "end", - "start" - ], - "type": "object" - }, - "CodexErrorInfo": { - "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", - "oneOf": [ - { - "enum": [ - "contextWindowExceeded", - "sessionBudgetExceeded", - "usageLimitExceeded", - "rateLimitExceeded", - "serverOverloaded", - "cyberPolicy", - "misalignmentPolicyViolation", - "internalServerError", - "unauthorized", - "badRequest", - "threadRollbackFailed", - "sandboxError", - "other" - ], - "type": "string" - }, - { - "additionalProperties": false, - "properties": { - "httpConnectionFailed": { - "properties": { - "httpStatusCode": { - "format": "uint16", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] - } - }, - "type": "object" - } - }, - "required": [ - "httpConnectionFailed" - ], - "title": "HttpConnectionFailedCodexErrorInfo", - "type": "object" - }, - { - "additionalProperties": false, - "description": "Failed to connect to the response SSE stream.", - "properties": { - "responseStreamConnectionFailed": { - "properties": { - "httpStatusCode": { - "format": "uint16", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] - } - }, - "type": "object" - } - }, - "required": [ - "responseStreamConnectionFailed" - ], - "title": "ResponseStreamConnectionFailedCodexErrorInfo", - "type": "object" - }, - { - "additionalProperties": false, - "description": "The response SSE stream disconnected in the middle of a turn before completion.", - "properties": { - "responseStreamDisconnected": { - "properties": { - "httpStatusCode": { - "format": "uint16", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] - } - }, - "type": "object" - } - }, - "required": [ - "responseStreamDisconnected" - ], - "title": "ResponseStreamDisconnectedCodexErrorInfo", - "type": "object" - }, - { - "additionalProperties": false, - "description": "Reached the retry limit for responses.", - "properties": { - "responseTooManyFailedAttempts": { - "properties": { - "httpStatusCode": { - "format": "uint16", - "minimum": 0.0, - "type": [ - "integer", - "null" - ] - } - }, - "type": "object" - } - }, - "required": [ - "responseTooManyFailedAttempts" - ], - "title": "ResponseTooManyFailedAttemptsCodexErrorInfo", - "type": "object" - }, - { - "additionalProperties": false, - "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", - "properties": { - "activeTurnNotSteerable": { - "properties": { - "turnKind": { - "$ref": "#/definitions/NonSteerableTurnKind" - } - }, - "required": [ - "turnKind" - ], - "type": "object" - } - }, - "required": [ - "activeTurnNotSteerable" - ], - "title": "ActiveTurnNotSteerableCodexErrorInfo", - "type": "object" - } - ] - }, - "CollabAgentState": { - "properties": { - "message": { - "type": [ - "string", - "null" - ] - }, - "status": { - "$ref": "#/definitions/CollabAgentStatus" - } - }, - "required": [ - "status" - ], - "type": "object" - }, - "CollabAgentStatus": { - "enum": [ - "pendingInit", - "running", - "interrupted", - "completed", - "errored", - "shutdown", - "notFound" - ], - "type": "string" - }, - "CollabAgentTool": { - "enum": [ - "spawnAgent", - "sendInput", - "resumeAgent", - "wait", - "closeAgent", - "sendMessage", - "followupTask", - "interruptAgent", - "listAgents" - ], - "type": "string" - }, - "CollabAgentToolCallStatus": { - "enum": [ - "inProgress", - "completed", - "failed", - "interrupted" - ], - "type": "string" - }, - "CommandAction": { - "oneOf": [ - { - "properties": { - "command": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "$ref": "#/definitions/LegacyAppPathString" - }, - "type": { - "enum": [ - "read" - ], - "title": "ReadCommandActionType", - "type": "string" - } - }, - "required": [ - "command", - "name", - "path", - "type" - ], - "title": "ReadCommandAction", - "type": "object" - }, - { - "properties": { - "command": { - "type": "string" - }, - "path": { - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "listFiles" - ], - "title": "ListFilesCommandActionType", - "type": "string" - } - }, - "required": [ - "command", - "type" - ], - "title": "ListFilesCommandAction", - "type": "object" - }, - { - "properties": { - "command": { - "type": "string" - }, - "path": { - "type": [ - "string", - "null" - ] - }, - "query": { - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "search" - ], - "title": "SearchCommandActionType", - "type": "string" - } - }, - "required": [ - "command", - "type" - ], - "title": "SearchCommandAction", - "type": "object" - }, - { - "properties": { - "command": { - "type": "string" - }, - "type": { - "enum": [ - "unknown" - ], - "title": "UnknownCommandActionType", - "type": "string" - } - }, - "required": [ - "command", - "type" - ], - "title": "UnknownCommandAction", - "type": "object" - } - ] - }, - "CommandExecutionSource": { - "enum": [ - "agent", - "userShell", - "unifiedExecStartup", - "unifiedExecInteraction" - ], - "type": "string" - }, - "CommandExecutionStatus": { - "enum": [ - "inProgress", - "completed", - "failed", - "declined" - ], - "type": "string" - }, - "DynamicToolCallOutputContentItem": { - "oneOf": [ - { - "properties": { - "text": { - "type": "string" - }, - "type": { - "enum": [ - "inputText" - ], - "title": "InputTextDynamicToolCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "title": "InputTextDynamicToolCallOutputContentItem", - "type": "object" - }, - { - "properties": { - "imageUrl": { - "type": "string" - }, - "type": { - "enum": [ - "inputImage" - ], - "title": "InputImageDynamicToolCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "imageUrl", - "type" - ], - "title": "InputImageDynamicToolCallOutputContentItem", - "type": "object" - }, - { - "properties": { - "audioUrl": { - "type": "string" - }, - "type": { - "enum": [ - "inputAudio" - ], - "title": "InputAudioDynamicToolCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "audioUrl", - "type" - ], - "title": "InputAudioDynamicToolCallOutputContentItem", - "type": "object" - } - ] - }, - "DynamicToolCallStatus": { - "enum": [ - "inProgress", - "completed", - "failed" - ], - "type": "string" - }, - "FileUpdateChange": { - "properties": { - "diff": { - "type": "string" - }, - "kind": { - "$ref": "#/definitions/PatchChangeKind" - }, - "path": { - "type": "string" - } - }, - "required": [ - "diff", - "kind", - "path" - ], - "type": "object" - }, - "FunctionCallOutputBody": { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "$ref": "#/definitions/FunctionCallOutputContentItem" - }, - "type": "array" - } - ] - }, - "FunctionCallOutputContentItem": { - "description": "Responses API compatible content items that can be returned by a tool call. This is a subset of ContentItem with the types we support as function call outputs.", - "oneOf": [ - { - "properties": { - "text": { - "type": "string" - }, - "type": { - "enum": [ - "input_text" - ], - "title": "InputTextFunctionCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "title": "InputTextFunctionCallOutputContentItem", - "type": "object" - }, - { - "properties": { - "detail": { - "anyOf": [ - { - "$ref": "#/definitions/ImageDetail" - }, - { - "type": "null" - } - ] - }, - "image_url": { - "type": "string" - }, - "type": { - "enum": [ - "input_image" - ], - "title": "InputImageFunctionCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "image_url", - "type" - ], - "title": "InputImageFunctionCallOutputContentItem", - "type": "object" - }, - { - "properties": { - "audio_url": { - "type": "string" - }, - "type": { - "enum": [ - "input_audio" - ], - "title": "InputAudioFunctionCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "audio_url", - "type" - ], - "title": "InputAudioFunctionCallOutputContentItem", - "type": "object" - }, - { - "properties": { - "encrypted_content": { - "type": "string" - }, - "type": { - "enum": [ - "encrypted_content" - ], - "title": "EncryptedContentFunctionCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "encrypted_content", - "type" - ], - "title": "EncryptedContentFunctionCallOutputContentItem", - "type": "object" - } - ] - }, - "GitInfo": { - "properties": { - "branch": { - "type": [ - "string", - "null" - ] - }, - "originUrl": { - "type": [ - "string", - "null" - ] - }, - "sha": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "HookPromptFragment": { - "properties": { - "hookRunId": { - "type": "string" - }, - "text": { - "type": "string" - } - }, - "required": [ - "hookRunId", - "text" - ], - "type": "object" - }, - "ImageDetail": { - "enum": [ - "auto", - "low", - "high", - "original" - ], - "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" - }, - "McpToolCallAppContext": { - "properties": { - "actionName": { - "type": [ - "string", - "null" - ] - }, - "appName": { - "type": [ - "string", - "null" - ] - }, - "connectorId": { - "type": "string" - }, - "linkId": { - "type": [ - "string", - "null" - ] - }, - "resourceUri": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "connectorId" - ], - "type": "object" - }, - "McpToolCallError": { - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "McpToolCallResult": { - "properties": { - "_meta": true, - "content": { - "items": true, - "type": "array" - }, - "structuredContent": true - }, - "required": [ - "content" - ], - "type": "object" - }, - "McpToolCallStatus": { - "enum": [ - "inProgress", - "completed", - "failed" - ], - "type": "string" - }, - "MemoryCitation": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/MemoryCitationEntry" - }, - "type": "array" - }, - "threadIds": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "entries", - "threadIds" - ], - "type": "object" - }, - "MemoryCitationEntry": { - "properties": { - "lineEnd": { - "format": "uint32", - "minimum": 0.0, - "type": "integer" - }, - "lineStart": { - "format": "uint32", - "minimum": 0.0, - "type": "integer" - }, - "note": { - "type": "string" - }, - "path": { - "type": "string" - } - }, - "required": [ - "lineEnd", - "lineStart", - "note", - "path" - ], - "type": "object" - }, - "MessagePhase": { - "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", - "oneOf": [ - { - "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", - "enum": [ - "commentary" - ], - "type": "string" - }, - { - "description": "The assistant's terminal answer text for the current turn.", - "enum": [ - "final_answer" - ], - "type": "string" - } - ] - }, - "MisalignmentErrorDetails": { - "properties": { - "detailedExplanation": { - "description": "A substantive localized explanation is required before offering continuation.", - "type": [ - "string", - "null" - ] - }, - "errorType": { - "description": "Open-ended classification; clients must accept categories added by Responses.", - "type": [ - "string", - "null" - ] - }, - "steer": { - "anyOf": [ - { - "$ref": "#/definitions/MisalignmentSteer" - }, - { - "type": "null" - } - ], - "description": "Instruction to submit as the next turn's user input if continuation is confirmed." - } - }, - "type": "object" - }, - "MisalignmentSteer": { - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "NonSteerableTurnKind": { - "enum": [ - "review", - "compact" - ], - "type": "string" - }, - "PatchApplyStatus": { - "enum": [ - "inProgress", - "completed", - "failed", - "declined" - ], - "type": "string" - }, - "PatchChangeKind": { - "oneOf": [ - { - "properties": { - "type": { - "enum": [ - "add" - ], - "title": "AddPatchChangeKindType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "AddPatchChangeKind", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "delete" - ], - "title": "DeletePatchChangeKindType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "DeletePatchChangeKind", - "type": "object" - }, - { - "properties": { - "move_path": { - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "update" - ], - "title": "UpdatePatchChangeKindType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "UpdatePatchChangeKind", - "type": "object" - } - ] - }, - "ReasoningEffort": { - "description": "A non-empty reasoning effort value advertised by the model.", - "minLength": 1, - "type": "string" - }, - "SessionSource": { - "oneOf": [ - { - "enum": [ - "cli", - "vscode", - "exec", - "appServer", - "unknown" - ], - "type": "string" - }, - { - "additionalProperties": false, - "properties": { - "custom": { - "type": "string" - } - }, - "required": [ - "custom" - ], - "title": "CustomSessionSource", - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "subAgent": { - "$ref": "#/definitions/SubAgentSource" - } - }, - "required": [ - "subAgent" - ], - "title": "SubAgentSessionSource", - "type": "object" - } - ] - }, - "SubAgentActivityKind": { - "enum": [ - "started", - "interacted", - "interrupted", - "completed" - ], - "type": "string" - }, - "SubAgentSource": { - "oneOf": [ - { - "enum": [ - "review", - "compact", - "memory_consolidation" - ], - "type": "string" - }, - { - "additionalProperties": false, - "properties": { - "thread_spawn": { - "properties": { - "agent_nickname": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "agent_path": { - "anyOf": [ - { - "$ref": "#/definitions/AgentPath" - }, - { - "type": "null" - } - ], - "default": null - }, - "agent_role": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "depth": { - "format": "int32", - "type": "integer" - }, - "parent_thread_id": { - "$ref": "#/definitions/ThreadId" - } - }, - "required": [ - "depth", - "parent_thread_id" - ], - "type": "object" - } - }, - "required": [ - "thread_spawn" - ], - "title": "ThreadSpawnSubAgentSource", - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "other": { - "type": "string" - } - }, - "required": [ - "other" - ], - "title": "OtherSubAgentSource", - "type": "object" - } - ] - }, - "TextElement": { - "properties": { - "byteRange": { - "allOf": [ - { - "$ref": "#/definitions/ByteRange" - } - ], - "description": "Byte range in the parent `text` buffer that this element occupies." - }, - "placeholder": { - "description": "Optional human-readable placeholder for the element, displayed in the UI.", - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "byteRange" - ], - "type": "object" - }, - "Thread": { - "properties": { - "agentNickname": { - "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", - "type": [ - "string", - "null" - ] - }, - "agentRole": { - "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", - "type": [ - "string", - "null" - ] - }, - "cliVersion": { - "description": "Version of the CLI that created the thread.", - "type": "string" - }, - "createdAt": { - "description": "Unix timestamp (in seconds) when the thread was created.", - "format": "int64", - "type": "integer" - }, - "cwd": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Working directory captured for the thread." - }, - "ephemeral": { - "description": "Whether the thread is ephemeral and should not be materialized on disk.", - "type": "boolean" - }, - "forkedFromId": { - "description": "Source thread id when this thread was created by forking another thread.", - "type": [ - "string", - "null" - ] - }, - "gitInfo": { - "anyOf": [ - { - "$ref": "#/definitions/GitInfo" - }, - { - "type": "null" - } - ], - "description": "Optional Git metadata captured when the thread was created." - }, - "historyMode": { - "allOf": [ - { - "$ref": "#/definitions/ThreadHistoryMode" - } - ], - "default": "legacy", - "description": "Persisted thread history contract selected when this thread was created." - }, - "id": { - "description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", - "type": "string" - }, - "model": { - "description": "Current configured model when loaded, otherwise the latest persisted model. Null when unavailable. This is not per-turn execution telemetry.", - "type": [ - "string", - "null" - ] - }, - "modelProvider": { - "description": "Model provider used for this thread (for example, 'openai').", - "type": "string" - }, - "name": { - "description": "Optional user-facing thread title.", - "type": [ - "string", - "null" - ] - }, - "originator": { - "description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.", - "type": [ - "string", - "null" - ] - }, - "parentThreadId": { - "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", - "type": [ - "string", - "null" - ] - }, - "path": { - "description": "[UNSTABLE] Path to the thread on disk.", - "type": [ - "string", - "null" - ] - }, - "preview": { - "description": "Usually the first user message in the thread, if available.", - "type": "string" - }, - "projectId": { - "description": "Canonical project assignment owned by app-server, if any.", - "type": [ - "string", - "null" - ] - }, - "reasoningEffort": { - "anyOf": [ - { - "$ref": "#/definitions/ReasoningEffort" - }, - { - "type": "null" - } - ], - "description": "Current configured reasoning effort when loaded, otherwise the latest persisted effort. Null when unset or unavailable. This is not per-turn execution telemetry." - }, - "recencyAt": { - "description": "Unix timestamp (in seconds) used for thread recency ordering.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "section": { - "anyOf": [ - { - "$ref": "#/definitions/ThreadSection" - }, - { - "type": "null" - } - ], - "default": null, - "description": "The independently persisted section selected for this thread, if any." - }, - "sectionEnteredAt": { - "default": null, - "description": "Unix timestamp in seconds when the thread entered its current section.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "sessionId": { - "description": "Session id shared by threads that belong to the same session tree.", - "type": "string" - }, - "source": { - "allOf": [ - { - "$ref": "#/definitions/SessionSource" - } - ], - "description": "Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.)." - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/ThreadStatus" - } - ], - "description": "Current runtime status for the thread." - }, - "threadSource": { - "anyOf": [ - { - "$ref": "#/definitions/ThreadSource" - }, - { - "type": "null" - } - ], - "description": "Optional analytics source classification for this thread." - }, - "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", - "items": { - "$ref": "#/definitions/Turn" - }, - "type": "array" - }, - "updatedAt": { - "description": "Unix timestamp (in seconds) when the thread was last updated.", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "cliVersion", - "createdAt", - "cwd", - "ephemeral", - "id", - "modelProvider", - "preview", - "projectId", - "sessionId", - "source", - "status", - "turns", - "updatedAt" - ], - "type": "object" - }, - "ThreadActiveFlag": { - "enum": [ - "waitingOnApproval", - "waitingOnUserInput" - ], - "type": "string" - }, - "ThreadEnvironment": { - "description": "An environment selected by a loaded thread, independent of connection status.", - "properties": { - "cwd": { - "$ref": "#/definitions/LegacyAppPathString" - }, - "environmentId": { - "type": "string" - }, - "runtimeWorkspaceRoots": { - "items": { - "$ref": "#/definitions/LegacyAppPathString" - }, - "type": "array" - } - }, - "required": [ - "cwd", - "environmentId", - "runtimeWorkspaceRoots" - ], - "type": "object" - }, - "ThreadExtra": { - "description": "Extra app-server data for a thread.", - "type": "object" - }, - "ThreadHistoryMode": { - "enum": [ - "legacy", - "paginated" - ], - "type": "string" - }, - "ThreadId": { - "type": "string" - }, - "ThreadItem": { - "oneOf": [ - { - "properties": { - "clientId": { - "type": [ - "string", - "null" - ] - }, - "content": { - "items": { - "$ref": "#/definitions/UserInput" - }, - "type": "array" - }, - "id": { - "type": "string" - }, - "type": { - "enum": [ - "userMessage" - ], - "title": "UserMessageThreadItemType", - "type": "string" - } - }, - "required": [ - "content", - "id", - "type" - ], - "title": "UserMessageThreadItem", - "type": "object" - }, - { - "properties": { - "fragments": { - "items": { - "$ref": "#/definitions/HookPromptFragment" - }, - "type": "array" - }, - "id": { - "type": "string" - }, - "type": { - "enum": [ - "hookPrompt" - ], - "title": "HookPromptThreadItemType", - "type": "string" - } - }, - "required": [ - "fragments", - "id", - "type" - ], - "title": "HookPromptThreadItem", - "type": "object" - }, - { - "properties": { - "delivery": { - "anyOf": [ - { - "$ref": "#/definitions/AgentMessageDelivery" - }, - { - "type": "null" - } - ], - "default": null - }, - "id": { - "type": "string" - }, - "memoryCitation": { - "anyOf": [ - { - "$ref": "#/definitions/MemoryCitation" - }, - { - "type": "null" - } - ], - "default": null - }, - "phase": { - "anyOf": [ - { - "$ref": "#/definitions/MessagePhase" - }, - { - "type": "null" - } - ], - "default": null - }, - "questions": { - "default": null, - "items": { - "$ref": "#/definitions/AsyncUserInputQuestion" - }, - "type": [ - "array", - "null" - ] - }, - "text": { - "type": "string" - }, - "type": { - "enum": [ - "agentMessage" - ], - "title": "AgentMessageThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "text", - "type" - ], - "title": "AgentMessageThreadItem", - "type": "object" - }, - { - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "namespace": { - "type": [ - "string", - "null" - ] - }, - "output": { - "$ref": "#/definitions/FunctionCallOutputBody" - }, - "type": { - "enum": [ - "functionCallOutput" - ], - "title": "FunctionCallOutputThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "name", - "output", - "type" - ], - "title": "FunctionCallOutputThreadItem", - "type": "object" - }, - { - "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", - "properties": { - "id": { - "type": "string" - }, - "text": { - "type": "string" - }, - "type": { - "enum": [ - "plan" - ], - "title": "PlanThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "text", - "type" - ], - "title": "PlanThreadItem", - "type": "object" - }, - { - "properties": { - "content": { - "default": [], - "items": { - "type": "string" - }, - "type": "array" - }, - "id": { - "type": "string" - }, - "summary": { - "default": [], - "items": { - "type": "string" - }, - "type": "array" - }, - "type": { - "enum": [ - "reasoning" - ], - "title": "ReasoningThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "title": "ReasoningThreadItem", - "type": "object" - }, - { - "properties": { - "aggregatedOutput": { - "description": "The command's output, aggregated from stdout and stderr.", - "type": [ - "string", - "null" - ] - }, - "command": { - "description": "The command to be executed.", - "type": "string" - }, - "commandActions": { - "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", - "items": { - "$ref": "#/definitions/CommandAction" - }, - "type": "array" - }, - "cwd": { - "allOf": [ - { - "$ref": "#/definitions/LegacyAppPathString" - } - ], - "description": "The command's working directory." - }, - "durationMs": { - "description": "The duration of the command execution in milliseconds.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "exitCode": { - "description": "The command's exit code.", - "format": "int32", - "type": [ - "integer", - "null" - ] - }, - "id": { - "type": "string" - }, - "pluginId": { - "default": null, - "description": "Trusted first-party plugin id when this command resolves to one plugin script.", - "type": [ - "string", - "null" - ] - }, - "processId": { - "description": "Identifier for the underlying PTY process (when available).", - "type": [ - "string", - "null" - ] - }, - "scriptPath": { - "default": null, - "description": "Safe plugin-relative path when this command resolves to one plugin script.", - "type": [ - "string", - "null" - ] - }, - "source": { - "allOf": [ - { - "$ref": "#/definitions/CommandExecutionSource" - } - ], - "default": "agent" - }, - "status": { - "$ref": "#/definitions/CommandExecutionStatus" - }, - "type": { - "enum": [ - "commandExecution" - ], - "title": "CommandExecutionThreadItemType", - "type": "string" - } - }, - "required": [ - "command", - "commandActions", - "cwd", - "id", - "status", - "type" - ], - "title": "CommandExecutionThreadItem", - "type": "object" - }, - { - "properties": { - "changes": { - "items": { - "$ref": "#/definitions/FileUpdateChange" - }, - "type": "array" - }, - "id": { - "type": "string" - }, - "status": { - "$ref": "#/definitions/PatchApplyStatus" - }, - "type": { - "enum": [ - "fileChange" - ], - "title": "FileChangeThreadItemType", - "type": "string" - } - }, - "required": [ - "changes", - "id", - "status", - "type" - ], - "title": "FileChangeThreadItem", - "type": "object" - }, - { - "properties": { - "appContext": { - "anyOf": [ - { - "$ref": "#/definitions/McpToolCallAppContext" - }, - { - "type": "null" - } - ] - }, - "arguments": true, - "durationMs": { - "description": "The duration of the MCP tool call in milliseconds.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/McpToolCallError" - }, - { - "type": "null" - } - ] - }, - "id": { - "type": "string" - }, - "mcpAppResourceUri": { - "description": "Deprecated: use `appContext.resourceUri` instead.", - "type": [ - "string", - "null" - ] - }, - "pluginId": { - "type": [ - "string", - "null" - ] - }, - "readOnlyHint": { - "type": [ - "boolean", - "null" - ] - }, - "result": { - "anyOf": [ - { - "$ref": "#/definitions/McpToolCallResult" - }, - { - "type": "null" - } - ] - }, - "server": { - "type": "string" - }, - "status": { - "$ref": "#/definitions/McpToolCallStatus" - }, - "tool": { - "type": "string" - }, - "type": { - "enum": [ - "mcpToolCall" - ], - "title": "McpToolCallThreadItemType", - "type": "string" - } - }, - "required": [ - "arguments", - "id", - "server", - "status", - "tool", - "type" - ], - "title": "McpToolCallThreadItem", - "type": "object" - }, - { - "properties": { - "arguments": true, - "contentItems": { - "items": { - "$ref": "#/definitions/DynamicToolCallOutputContentItem" - }, - "type": [ - "array", - "null" - ] - }, - "durationMs": { - "description": "The duration of the dynamic tool call in milliseconds.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "id": { - "type": "string" - }, - "namespace": { - "type": [ - "string", - "null" - ] - }, - "status": { - "$ref": "#/definitions/DynamicToolCallStatus" - }, - "success": { - "type": [ - "boolean", - "null" - ] - }, - "tool": { - "type": "string" - }, - "type": { - "enum": [ - "dynamicToolCall" - ], - "title": "DynamicToolCallThreadItemType", - "type": "string" - } - }, - "required": [ - "arguments", - "id", - "status", - "tool", - "type" - ], - "title": "DynamicToolCallThreadItem", - "type": "object" - }, - { - "properties": { - "agentsStates": { - "additionalProperties": { - "$ref": "#/definitions/CollabAgentState" - }, - "description": "Last known status of the target agents, when available.", - "type": "object" - }, - "id": { - "description": "Unique identifier for this collab tool call.", - "type": "string" - }, - "model": { - "description": "Model requested for the spawned agent, when applicable.", - "type": [ - "string", - "null" - ] - }, - "prompt": { - "description": "Prompt text sent as part of the collab tool call, when available.", - "type": [ - "string", - "null" - ] - }, - "reasoningEffort": { - "anyOf": [ - { - "$ref": "#/definitions/ReasoningEffort" - }, - { - "type": "null" - } - ], - "description": "Reasoning effort requested for the spawned agent, when applicable." - }, - "receiverThreadIds": { - "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", - "items": { - "type": "string" - }, - "type": "array" - }, - "senderThreadId": { - "description": "Thread ID of the agent issuing the collab request.", - "type": "string" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/CollabAgentToolCallStatus" - } - ], - "description": "Current status of the collab tool call." - }, - "tool": { - "allOf": [ - { - "$ref": "#/definitions/CollabAgentTool" - } - ], - "description": "Name of the collab tool that was invoked." - }, - "type": { - "enum": [ - "collabAgentToolCall" - ], - "title": "CollabAgentToolCallThreadItemType", - "type": "string" - } - }, - "required": [ - "agentsStates", - "id", - "receiverThreadIds", - "senderThreadId", - "status", - "tool", - "type" - ], - "title": "CollabAgentToolCallThreadItem", - "type": "object" - }, - { - "properties": { - "agentPath": { - "type": "string" - }, - "agentThreadId": { - "type": "string" - }, - "id": { - "type": "string" - }, - "kind": { - "$ref": "#/definitions/SubAgentActivityKind" - }, - "type": { - "enum": [ - "subAgentActivity" - ], - "title": "SubAgentActivityThreadItemType", - "type": "string" - } - }, - "required": [ - "agentPath", - "agentThreadId", - "id", - "kind", - "type" - ], - "title": "SubAgentActivityThreadItem", - "type": "object" - }, - { - "properties": { - "action": { - "anyOf": [ - { - "$ref": "#/definitions/WebSearchAction" - }, - { - "type": "null" - } - ] - }, - "id": { - "type": "string" - }, - "query": { - "type": "string" - }, - "results": { - "default": null, - "description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", - "items": true, - "type": [ - "array", - "null" - ] - }, - "type": { - "enum": [ - "webSearch" - ], - "title": "WebSearchThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "query", - "type" - ], - "title": "WebSearchThreadItem", - "type": "object" - }, - { - "properties": { - "id": { - "type": "string" - }, - "path": { - "$ref": "#/definitions/LegacyAppPathString" - }, - "type": { - "enum": [ - "imageView" - ], - "title": "ImageViewThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "path", - "type" - ], - "title": "ImageViewThreadItem", - "type": "object" - }, - { - "description": "Display item emitted by the interruptible `clock.sleep` tool.", - "properties": { - "durationMs": { - "format": "uint64", - "minimum": 0.0, - "type": "integer" - }, - "id": { - "type": "string" - }, - "type": { - "enum": [ - "sleep" - ], - "title": "SleepThreadItemType", - "type": "string" - } - }, - "required": [ - "durationMs", - "id", - "type" - ], - "title": "SleepThreadItem", - "type": "object" - }, - { - "properties": { - "failure": { - "anyOf": [ - { - "$ref": "#/definitions/ImageGenerationFailure" - }, - { - "type": "null" - } - ], - "default": null - }, - "id": { - "type": "string" - }, - "result": { - "type": "string" - }, - "revisedPrompt": { - "type": [ - "string", - "null" - ] - }, - "savedPath": { - "anyOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - }, - { - "type": "null" - } - ] - }, - "status": { - "type": "string" - }, - "transparentBackground": { - "default": null, - "type": [ - "boolean", - "null" - ] - }, - "type": { - "enum": [ - "imageGeneration" - ], - "title": "ImageGenerationThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "result", - "status", - "type" - ], - "title": "ImageGenerationThreadItem", - "type": "object" - }, - { - "properties": { - "id": { - "type": "string" - }, - "review": { - "type": "string" - }, - "type": { - "enum": [ - "enteredReviewMode" - ], - "title": "EnteredReviewModeThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "review", - "type" - ], - "title": "EnteredReviewModeThreadItem", - "type": "object" - }, - { - "properties": { - "id": { - "type": "string" - }, - "review": { - "type": "string" - }, - "type": { - "enum": [ - "exitedReviewMode" - ], - "title": "ExitedReviewModeThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "review", - "type" - ], - "title": "ExitedReviewModeThreadItem", - "type": "object" - }, - { - "properties": { - "id": { - "type": "string" - }, - "type": { - "enum": [ - "contextCompaction" - ], - "title": "ContextCompactionThreadItemType", - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "title": "ContextCompactionThreadItem", - "type": "object" - } - ] - }, - "ThreadSection": { - "description": "An independently persisted, user-visible thread section.", - "properties": { - "appearance": { - "anyOf": [ - { - "$ref": "#/definitions/ThreadSectionAppearance" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Optional appearance synchronized across clients." - }, - "id": { - "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", - "type": "string" - }, - "name": { - "description": "The current user-visible section name.", - "type": "string" - } - }, - "required": [ - "id", - "name" - ], - "type": "object" - }, - "ThreadSectionAppearance": { - "description": "Extensible visual presentation for a custom thread section.", - "properties": { - "color": { - "type": [ - "string", - "null" - ] - }, - "icon": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "ThreadSource": { - "type": "string" - }, - "ThreadStatus": { - "oneOf": [ - { - "properties": { - "type": { - "enum": [ - "notLoaded" - ], - "title": "NotLoadedThreadStatusType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "NotLoadedThreadStatus", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "idle" - ], - "title": "IdleThreadStatusType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "IdleThreadStatus", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "systemError" - ], - "title": "SystemErrorThreadStatusType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "SystemErrorThreadStatus", - "type": "object" - }, - { - "properties": { - "activeFlags": { - "items": { - "$ref": "#/definitions/ThreadActiveFlag" - }, - "type": "array" - }, - "type": { - "enum": [ - "active" - ], - "title": "ActiveThreadStatusType", - "type": "string" - } - }, - "required": [ - "activeFlags", - "type" - ], - "title": "ActiveThreadStatus", - "type": "object" - } - ] - }, - "Turn": { - "properties": { - "completedAt": { - "description": "Unix timestamp (in seconds) when the turn completed.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "durationMs": { - "description": "Duration between turn start and completion in milliseconds, if known.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "error": { - "anyOf": [ - { - "$ref": "#/definitions/TurnError" - }, - { - "type": "null" - } - ], - "description": "Only populated when the Turn's status is failed." - }, - "id": { - "description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", - "type": "string" - }, - "items": { - "description": "Thread items currently included in this turn payload.", - "items": { - "$ref": "#/definitions/ThreadItem" - }, - "type": "array" - }, - "itemsView": { - "allOf": [ - { - "$ref": "#/definitions/TurnItemsView" - } - ], - "default": "full", - "description": "Describes how much of `items` has been loaded for this turn." - }, - "startedAt": { - "description": "Unix timestamp (in seconds) when the turn started.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "status": { - "$ref": "#/definitions/TurnStatus" - } - }, - "required": [ - "id", - "items", - "status" - ], - "type": "object" - }, - "TurnError": { - "properties": { - "additionalDetails": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "codexErrorInfo": { - "anyOf": [ - { - "$ref": "#/definitions/CodexErrorInfo" - }, - { - "type": "null" - } - ] - }, - "message": { - "type": "string" - }, - "misalignment": { - "anyOf": [ - { - "$ref": "#/definitions/MisalignmentErrorDetails" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Optional public explanation and continuation instruction for a misalignment block." - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "TurnItemsView": { - "oneOf": [ - { - "description": "`items` was not loaded for this turn. The field is intentionally empty.", - "enum": [ - "notLoaded" - ], - "type": "string" - }, - { - "description": "`items` contains only a display summary for this turn.", - "enum": [ - "summary" - ], - "type": "string" - }, - { - "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", - "enum": [ - "full" - ], - "type": "string" - } - ] - }, - "TurnStatus": { - "enum": [ - "completed", - "interrupted", - "failed", - "inProgress" - ], - "type": "string" - }, - "UserInput": { - "oneOf": [ - { - "properties": { - "text": { - "type": "string" - }, - "text_elements": { - "default": [], - "description": "UI-defined spans within `text` used to render or persist special elements.", - "items": { - "$ref": "#/definitions/TextElement" - }, - "type": "array" - }, - "type": { - "enum": [ - "text" - ], - "title": "TextUserInputType", - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "title": "TextUserInput", - "type": "object" - }, - { - "properties": { - "detail": { - "anyOf": [ - { - "$ref": "#/definitions/ImageDetail" - }, - { - "type": "null" - } - ], - "default": null - }, - "type": { - "enum": [ - "image" - ], - "title": "ImageUserInputType", - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "type", - "url" - ], - "title": "ImageUserInput", - "type": "object" - }, - { - "properties": { - "detail": { - "anyOf": [ - { - "$ref": "#/definitions/ImageDetail" - }, - { - "type": "null" - } - ], - "default": null - }, - "path": { - "type": "string" - }, - "type": { - "enum": [ - "localImage" - ], - "title": "LocalImageUserInputType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "LocalImageUserInput", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "audio" - ], - "title": "AudioUserInputType", - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "type", - "url" - ], - "title": "AudioUserInput", - "type": "object" - }, - { - "properties": { - "path": { - "type": "string" - }, - "type": { - "enum": [ - "localAudio" - ], - "title": "LocalAudioUserInputType", - "type": "string" - } - }, - "required": [ - "path", - "type" - ], - "title": "LocalAudioUserInput", - "type": "object" - }, - { - "properties": { - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "type": { - "enum": [ - "skill" - ], - "title": "SkillUserInputType", - "type": "string" - } - }, - "required": [ - "name", - "path", - "type" - ], - "title": "SkillUserInput", - "type": "object" - }, - { - "properties": { - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "type": { - "enum": [ - "mention" - ], - "title": "MentionUserInputType", - "type": "string" - } - }, - "required": [ - "name", - "path", - "type" - ], - "title": "MentionUserInput", - "type": "object" - } - ] - }, - "WebSearchAction": { - "oneOf": [ - { - "properties": { - "queries": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "query": { - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "search" - ], - "title": "SearchWebSearchActionType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "SearchWebSearchAction", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "openPage" - ], - "title": "OpenPageWebSearchActionType", - "type": "string" - }, - "url": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "type" - ], - "title": "OpenPageWebSearchAction", - "type": "object" - }, - { - "properties": { - "pattern": { - "type": [ - "string", - "null" - ] - }, - "type": { - "enum": [ - "findInPage" - ], - "title": "FindInPageWebSearchActionType", - "type": "string" - }, - "url": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "type" - ], - "title": "FindInPageWebSearchAction", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "other" - ], - "title": "OtherWebSearchActionType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "OtherWebSearchAction", - "type": "object" - } - ] - } - }, - "properties": { - "thread": { - "allOf": [ - { - "$ref": "#/definitions/Thread" - } - ], - "description": "The updated thread after applying the rollback, with `turns` populated.\n\nThe ThreadItems stored in each Turn are lossy since we explicitly do not persist all agent interactions, such as command executions. This is the same behavior as `thread/resume`." - } - }, - "required": [ - "thread" - ], - "title": "ThreadRollbackResponse", - "type": "object" -} \ No newline at end of file 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 a39db2c15d..3f193b0de8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1456,7 +1456,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, 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 bcfd63f923..e7eb265dcb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1247,7 +1247,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, 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 2ed8f65ebe..7c0f053e20 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1247,7 +1247,7 @@ "description": "Optional analytics source classification for this thread." }, "turns": { - "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "description": "Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", "items": { "$ref": "#/definitions/Turn" }, 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 88c9c28a76..8a4f90d7ef 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 416d36cff5..0e73285dac 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/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index e6366163cf..45ed6aa96c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -85,7 +85,6 @@ import type { ThreadMetadataUpdateParams } from "./v2/ThreadMetadataUpdateParams import type { ThreadReadParams } from "./v2/ThreadReadParams"; import type { ThreadResumeParams } from "./v2/ThreadResumeParams"; import type { ThreadRevertParams } from "./v2/ThreadRevertParams"; -import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams"; import type { ThreadSectionCreateParams } from "./v2/ThreadSectionCreateParams"; import type { ThreadSectionDeleteParams } from "./v2/ThreadSectionDeleteParams"; import type { ThreadSectionListParams } from "./v2/ThreadSectionListParams"; @@ -105,4 +104,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/attachment/add", id: RequestId, params: ThreadAttachmentAddParams, } | { "method": "thread/attachment/list", id: RequestId, params: ThreadAttachmentListParams, } | { "method": "thread/attachment/remove", id: RequestId, params: ThreadAttachmentRemoveParams, } | { "method": "thread/section/move", id: RequestId, params: ThreadSectionMoveParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/revert", id: RequestId, params: ThreadRevertParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "threadSection/list", id: RequestId, params: ThreadSectionListParams, } | { "method": "threadSection/create", id: RequestId, params: ThreadSectionCreateParams, } | { "method": "threadSection/update", id: RequestId, params: ThreadSectionUpdateParams, } | { "method": "threadSection/delete", id: RequestId, params: ThreadSectionDeleteParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/items/list", id: RequestId, params: ThreadItemsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/reconcile", id: RequestId, params: PluginReconcileParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/read", id: RequestId, params: AppsReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "app/installed", id: RequestId, params: AppsInstalledParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params?: GetAccountRateLimitsParams | undefined, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params?: GetAccountTokenUsageParams | undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "externalAgentConfig/import/recordHistory", id: RequestId, params: ExternalAgentConfigImportHistoryRecordParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/attachment/add", id: RequestId, params: ThreadAttachmentAddParams, } | { "method": "thread/attachment/list", id: RequestId, params: ThreadAttachmentListParams, } | { "method": "thread/attachment/remove", id: RequestId, params: ThreadAttachmentRemoveParams, } | { "method": "thread/section/move", id: RequestId, params: ThreadSectionMoveParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/revert", id: RequestId, params: ThreadRevertParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "threadSection/list", id: RequestId, params: ThreadSectionListParams, } | { "method": "threadSection/create", id: RequestId, params: ThreadSectionCreateParams, } | { "method": "threadSection/update", id: RequestId, params: ThreadSectionUpdateParams, } | { "method": "threadSection/delete", id: RequestId, params: ThreadSectionDeleteParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/items/list", id: RequestId, params: ThreadItemsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/reconcile", id: RequestId, params: PluginReconcileParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/read", id: RequestId, params: AppsReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "app/installed", id: RequestId, params: AppsInstalledParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params?: GetAccountRateLimitsParams | undefined, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params?: GetAccountTokenUsageParams | undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "externalAgentConfig/import/recordHistory", id: RequestId, params: ExternalAgentConfigImportHistoryRecordParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts index 42a836746d..9a0f865d7b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts @@ -96,7 +96,7 @@ gitInfo: GitInfo | null, /** * Optional user-facing thread title. */ name: string | null, /** - * Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` + * Only populated on `thread/resume`, `thread/fork`, and `thread/read` * (when `includeTurns` is true) responses. * For all other responses and notifications returning a Thread, * the turns field will be an empty list. diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackParams.ts deleted file mode 100644 index af416d1872..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// 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. - -/** - * DEPRECATED: `thread/rollback` will be removed soon. - */ -export type ThreadRollbackParams = { threadId: string, -/** - * The number of turns to drop from the end of the thread. Must be >= 1. - * - * This only modifies the thread's history and does not revert local file changes - * that have been made by the agent. Clients are responsible for reverting these changes. - */ -numTurns: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackResponse.ts deleted file mode 100644 index 6597cc8178..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRollbackResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 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. -import type { Thread } from "./Thread"; - -export type ThreadRollbackResponse = { -/** - * The updated thread after applying the rollback, with `turns` populated. - * - * The ThreadItems stored in each Turn are lossy since we explicitly do not - * persist all agent interactions, such as command executions. This is the same - * behavior as `thread/resume`. - */ -thread: Thread, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index a537a41004..242c9fc3e2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -543,8 +543,6 @@ export type { ThreadResumeResponse } from "./ThreadResumeResponse"; export type { ThreadRevertParams } from "./ThreadRevertParams"; export type { ThreadRevertResponse } from "./ThreadRevertResponse"; export type { ThreadRevertedNotification } from "./ThreadRevertedNotification"; -export type { ThreadRollbackParams } from "./ThreadRollbackParams"; -export type { ThreadRollbackResponse } from "./ThreadRollbackResponse"; export type { ThreadSearchResult } from "./ThreadSearchResult"; export type { ThreadSearchSortKey } from "./ThreadSearchSortKey"; export type { ThreadSection } from "./ThreadSection"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 26ac59ec6b..a45b943055 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -753,11 +753,6 @@ client_request_definitions! { serialization: thread_id(params.thread_id), response: v2::ThreadBackgroundTerminalsTerminateResponse, }, - ThreadRollback => "thread/rollback" { - params: v2::ThreadRollbackParams, - serialization: thread_id(params.thread_id), - response: v2::ThreadRollbackResponse, - }, ThreadRevert => "thread/revert" { params: v2::ThreadRevertParams, serialization: thread_id(params.thread_id), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index f13ab99817..0d5a7b5a99 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -1241,31 +1241,6 @@ pub struct ThreadBackgroundTerminalsTerminateResponse { pub terminated: bool, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// DEPRECATED: `thread/rollback` will be removed soon. -pub struct ThreadRollbackParams { - pub thread_id: String, - /// The number of turns to drop from the end of the thread. Must be >= 1. - /// - /// This only modifies the thread's history and does not revert local file changes - /// that have been made by the agent. Clients are responsible for reverting these changes. - pub num_turns: u32, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRollbackResponse { - /// The updated thread after applying the rollback, with `turns` populated. - /// - /// The ThreadItems stored in each Turn are lossy since we explicitly do not - /// persist all agent interactions, such as command executions. This is the same - /// behavior as `thread/resume`. - pub thread: Thread, -} - /// Replace a paginated thread's durable history with the prefix before one turn. /// /// This only changes persisted conversation history. It does not revert local file changes. diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs index fbc0b70471..6d94cdd78b 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs @@ -286,7 +286,7 @@ pub struct Thread { /// Saved Daybreak choice, independent of turn execution. Null if unset. #[experimental("thread.daybreakEnabled")] pub daybreak_enabled: Option, - /// Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` + /// Only populated on `thread/resume`, `thread/fork`, and `thread/read` /// (when `includeTurns` is true) responses. /// For all other responses and notifications returning a Thread, /// the turns field will be an empty list. diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 75b318f7b6..e1fc57eaa0 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -204,3 +204,14 @@ in both `full` and `toolsAndAuthOnly` detail modes, including thread-scoped read This is the server's advertised MCP capabilities object, including its `extensions` map. It is null when the connection has not initialized successfully; capabilities are never inferred from tools or copied from a shared catalog cache. + +# Thread rollback + +`thread/rollback` has been removed from the API, including its request and response +types. Requests use the generic unknown-method rejection path. Use `thread/revert` +for paginated threads instead. + +Existing rollouts may contain historical `ThreadRolledBack` events. Their replay +and migration remain supported so resuming, reading, and forking those threads +preserves the surviving history. This disk compatibility does not require restoring +support for new `thread/rollback` requests. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index b5171d2320..27ae9505cd 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -1,11 +1,6 @@ -use crate::error_code::internal_error; -use crate::error_code::invalid_request; use crate::notification_media::without_notification_media; use crate::outgoing_message::ClientRequestResult; use crate::outgoing_message::ThreadScopedOutgoingMessageSender; -use crate::request_processors::apply_live_thread_settings; -use crate::request_processors::populate_thread_turns_from_history; -use crate::request_processors::thread_from_stored_thread; use crate::request_processors::thread_settings_from_config_snapshot; use crate::server_request_error::is_turn_transition_server_request_error; use crate::thread_state::ThreadState; @@ -70,9 +65,7 @@ use codex_app_server_protocol::ThreadRealtimeSdpNotification; use codex_app_server_protocol::ThreadRealtimeStartedNotification; use codex_app_server_protocol::ThreadRealtimeTranscriptDeltaNotification; use codex_app_server_protocol::ThreadRealtimeTranscriptDoneNotification; -use codex_app_server_protocol::ThreadRollbackResponse; use codex_app_server_protocol::ThreadSettingsUpdatedNotification; -use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::ThreadTokenUsage; use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification; use codex_app_server_protocol::ToolRequestUserInputOption; @@ -102,7 +95,6 @@ use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool; use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; use codex_protocol::plan_tool::UpdatePlanArgs; -use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; @@ -120,7 +112,6 @@ use codex_protocol::request_permissions::RequestPermissionsResponse as CoreReque use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse; use codex_shell_command::parse_command::shlex_join; -use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathUri; use std::collections::HashMap; @@ -145,7 +136,6 @@ struct CommandExecutionCompletionItem { command_actions: Vec, } -#[allow(clippy::too_many_arguments)] pub(crate) async fn apply_bespoke_event_handling( event: Event, conversation_id: ThreadId, @@ -154,8 +144,6 @@ pub(crate) async fn apply_bespoke_event_handling( outgoing: ThreadScopedOutgoingMessageSender, thread_state: Arc>, thread_watch_manager: ThreadWatchManager, - thread_list_state_permit: Arc, - fallback_model_provider: String, ) { let Event { id: event_turn_id, @@ -1036,25 +1024,6 @@ pub(crate) async fn apply_bespoke_event_handling( .note_system_error(&conversation_id.to_string()) .await; - let message = ev.message.clone(); - let codex_error_info = ev.codex_error_info.clone(); - // If this error belongs to an in-flight `thread/rollback` request, fail that request - // (and clear pending state) so subsequent rollbacks are unblocked. - // - // Don't send a notification for this error. - if matches!( - codex_error_info, - Some(CoreCodexErrorInfo::ThreadRollbackFailed) - ) { - return handle_thread_rollback_failed( - conversation_id, - message, - &thread_state, - &outgoing, - ) - .await; - }; - if !ev.affects_turn_status() { return; } @@ -1239,70 +1208,6 @@ pub(crate) async fn apply_bespoke_event_handling( ) .await; } - EventMsg::ThreadRolledBack(_rollback_event) => { - let pending = { - let mut state = thread_state.lock().await; - state.pending_rollbacks.take() - }; - - if let Some((request_id, _completion_tx)) = pending { - let _thread_list_state_permit = match thread_list_state_permit.acquire().await { - Ok(permit) => permit, - Err(err) => { - outgoing - .send_error( - request_id, - internal_error(format!( - "failed to acquire thread list state permit: {err}" - )), - ) - .await; - return; - } - }; - let config_snapshot = conversation.config_snapshot().await; - let stored_thread = match conversation - .read_thread( - /*include_archived*/ true, /*include_history*/ true, - ) - .await - { - Ok(stored_thread) => stored_thread, - Err(err) => { - outgoing - .send_error( - request_id.clone(), - internal_error(format!( - "failed to read thread {conversation_id} after rollback: {err}" - )), - ) - .await; - return; - } - }; - let loaded_status = thread_watch_manager - .loaded_status_for_thread(&conversation_id.to_string()) - .await; - let mut response = match thread_rollback_response_from_stored_thread( - stored_thread, - conversation.session_configured().session_id.to_string(), - fallback_model_provider.as_str(), - config_snapshot.cwd(), - loaded_status, - ) { - Ok(response) => response, - Err(err) => { - outgoing - .send_error(request_id.clone(), internal_error(err)) - .await; - return; - } - }; - - apply_live_thread_settings(&mut response.thread, &config_snapshot); - outgoing.send_response(request_id, response).await; - } - } EventMsg::ThreadGoalUpdated(thread_goal_event) => { let notification = ThreadGoalUpdatedNotification { thread_id: thread_goal_event.thread_id.to_string(), @@ -1315,7 +1220,7 @@ pub(crate) async fn apply_bespoke_event_handling( )) .await; } - EventMsg::ThreadQueueChanged(_) => {} + EventMsg::ThreadRolledBack(_) | EventMsg::ThreadQueueChanged(_) => {} EventMsg::ThreadSettingsApplied(_) => { let thread_settings = thread_settings_from_config_snapshot(&conversation.config_snapshot().await); @@ -1635,42 +1540,6 @@ async fn handle_turn_interrupted( .await; } -async fn handle_thread_rollback_failed( - _conversation_id: ThreadId, - message: String, - thread_state: &Arc>, - outgoing: &ThreadScopedOutgoingMessageSender, -) { - let pending_rollback = thread_state.lock().await.pending_rollbacks.take(); - - if let Some((request_id, _completion_tx)) = pending_rollback { - outgoing - .send_error(request_id, invalid_request(message)) - .await; - } -} - -fn thread_rollback_response_from_stored_thread( - stored_thread: codex_thread_store::StoredThread, - session_id: String, - fallback_model_provider: &str, - fallback_cwd: &AbsolutePathBuf, - loaded_status: ThreadStatus, -) -> std::result::Result { - let thread_id = stored_thread.thread_id; - let (mut thread, history) = - thread_from_stored_thread(stored_thread, fallback_model_provider, fallback_cwd); - thread.session_id = session_id; - let Some(history) = history else { - return Err(format!( - "thread {thread_id} did not include persisted history after rollback" - )); - }; - populate_thread_turns_from_history(&mut thread, &history.items, /*active_turn*/ None); - thread.status = loaded_status; - Ok(ThreadRollbackResponse { thread }) -} - async fn respond_to_pending_interrupts( thread_state: &Arc>, outgoing: &ThreadScopedOutgoingMessageSender, @@ -2219,11 +2088,11 @@ mod tests { use anyhow::Result; use anyhow::anyhow; use anyhow::bail; - use chrono::Utc; use codex_app_server_protocol::AutoReviewDecisionSource; use codex_app_server_protocol::GuardianApprovalReviewStatus; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ServerRequest; + use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::TurnPlanStepStatus; use codex_login::CodexAuth; use codex_protocol::AgentPath; @@ -2235,11 +2104,8 @@ mod tests { use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; - use codex_protocol::models::PermissionProfile; use codex_protocol::plan_tool::PlanItemArg; use codex_protocol::plan_tool::StepStatus; - use codex_protocol::protocol::AgentMessageEvent; - use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AuthRecoveryEvent; use codex_protocol::protocol::CreditsSnapshot; use codex_protocol::protocol::EventMsg; @@ -2249,13 +2115,8 @@ mod tests { use codex_protocol::protocol::ItemStartedEvent; use codex_protocol::protocol::RateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow; - use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; - use codex_protocol::protocol::UserMessageEvent; - use codex_rollout::RolloutItem; - use codex_thread_store::StoredThread; - use codex_thread_store::StoredThreadHistory; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::test_path_buf; @@ -2296,87 +2157,6 @@ mod tests { Ok(envelope.notification) } - #[test] - fn rollback_response_rebuilds_pathless_thread_from_stored_history() -> Result<()> { - let thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000789")?; - let created_at = Utc::now(); - let history_items = vec![ - RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { - client_id: None, - message: "before rollback".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - })), - RolloutItem::EventMsg(EventMsg::AgentMessage(AgentMessageEvent { - message: "after rollback".to_string(), - phase: None, - memory_citation: None, - delivery: None, - questions: None, - })), - ]; - let stored_thread = StoredThread { - originator: None, - thread_id, - extra_config: None, - rollout_path: None, - forked_from_id: None, - parent_thread_id: None, - preview: "fallback preview".to_string(), - name: Some("Rollback thread".to_string()), - model_provider: "openai".to_string(), - model: None, - reasoning_effort: None, - created_at, - updated_at: created_at, - recency_at: created_at, - archived_at: None, - section: None, - section_position: None, - section_entered_at: None, - project_id: None, - daybreak_enabled: None, - cwd: test_path_buf("/tmp").abs().into(), - cli_version: "0.0.0".to_string(), - source: SessionSource::Cli, - history_mode: Default::default(), - thread_source: None, - agent_nickname: None, - agent_role: None, - agent_path: None, - git_info: None, - approval_mode: AskForApproval::OnRequest, - permission_profile: PermissionProfile::read_only(), - token_usage: None, - first_user_message: Some("before rollback".to_string()), - history: Some(StoredThreadHistory { - thread_id, - items: history_items, - }), - }; - let fallback_cwd = test_path_buf("/tmp").abs(); - - let response = thread_rollback_response_from_stored_thread( - stored_thread, - thread_id.to_string(), - "fallback-provider", - &fallback_cwd, - ThreadStatus::NotLoaded, - ) - .expect("rollback response should rebuild from stored history"); - - assert_eq!(response.thread.id, thread_id.to_string()); - assert_eq!(response.thread.path, None); - assert_eq!(response.thread.preview, "fallback preview"); - assert_eq!(response.thread.name.as_deref(), Some("Rollback thread")); - assert_eq!(response.thread.status, ThreadStatus::NotLoaded); - assert_eq!(response.thread.turns.len(), 1); - assert_eq!(response.thread.turns[0].items.len(), 2); - Ok(()) - } - fn turn_complete_event(turn_id: &str) -> TurnCompleteEvent { TurnCompleteEvent { turn_id: turn_id.to_string(), @@ -2485,8 +2265,6 @@ mod tests { self.outgoing.clone(), self.thread_state.clone(), self.thread_watch_manager.clone(), - Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)), - "test-provider".to_string(), ) .await; } @@ -3330,8 +3108,6 @@ mod tests { outgoing.clone(), Arc::clone(&thread_state), thread_watch_manager.clone(), - Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)), - "test-provider".to_string(), ) .await; @@ -3372,8 +3148,6 @@ mod tests { outgoing.clone(), Arc::clone(&thread_state), thread_watch_manager.clone(), - Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)), - "test-provider".to_string(), ) .await; @@ -3453,8 +3227,6 @@ mod tests { outgoing, new_thread_state(), thread_watch_manager.clone(), - Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)), - "test-provider".to_string(), ) .await; @@ -3542,8 +3314,6 @@ mod tests { outgoing, new_thread_state(), ThreadWatchManager::new(), - Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)), - "test-provider".to_string(), ) .await; diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index be62b731bf..c0d2818bb9 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -528,7 +528,6 @@ impl MessageProcessor { pending_thread_unloads, thread_state_manager, thread_watch_manager, - thread_list_state_permit, Arc::clone(&skills_watcher), turn_cost_worker.as_ref().map(TurnCostWorker::handle), ); @@ -983,7 +982,6 @@ impl MessageProcessor { ClientRequest::ThreadStart { .. } | ClientRequest::ThreadFork { .. } | ClientRequest::ThreadResume { .. } - | ClientRequest::ThreadRollback { .. } | ClientRequest::ThreadRevert { .. } | ClientRequest::ThreadSettingsUpdate { .. } | ClientRequest::TurnSettingsUpdate { .. } @@ -1449,11 +1447,6 @@ impl MessageProcessor { .thread_background_terminals_terminate(params) .await } - ClientRequest::ThreadRollback { params, .. } => { - self.thread_processor - .thread_rollback(&request_id, params, app_server_client_name.as_deref()) - .await - } ClientRequest::ThreadRevert { params, .. } => { self.thread_processor .thread_revert( diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index ec84a83262..e6803f0b4d 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -231,14 +231,6 @@ impl ThreadScopedOutgoingMessageSender { { self.outgoing.send_response(request_id, response).await; } - - pub(crate) async fn send_error( - &self, - request_id: ConnectionRequestId, - error: impl Into, - ) { - self.outgoing.send_error(request_id, error).await; - } } impl OutgoingMessageSender { diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 6ec3567f2f..9e73f535a6 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -262,7 +262,6 @@ use codex_app_server_protocol::ThreadRealtimeStopResponse; use codex_app_server_protocol::ThreadResumeInitialTurnsPageParams; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; -use codex_app_server_protocol::ThreadRollbackParams; use codex_app_server_protocol::ThreadSearchOccurrence; use codex_app_server_protocol::ThreadSearchOccurrencesParams; use codex_app_server_protocol::ThreadSearchOccurrencesResponse; diff --git a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs index fbc4d32cf1..71fba6b25e 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -11,8 +11,6 @@ pub(super) struct ListenerTaskContext { pub(super) outgoing: Arc, pub(super) pending_thread_unloads: Arc>>, pub(super) thread_watch_manager: ThreadWatchManager, - pub(super) thread_list_state_permit: Arc, - pub(super) fallback_model_provider: String, pub(super) codex_home: PathBuf, pub(super) thread_unload_delay: Duration, pub(super) skills_watcher: Arc, @@ -271,8 +269,6 @@ pub(super) async fn ensure_listener_task_running( thread_state_manager, pending_thread_unloads, thread_watch_manager, - thread_list_state_permit, - fallback_model_provider, codex_home, turn_cost_worker, .. @@ -353,8 +349,6 @@ pub(super) async fn ensure_listener_task_running( thread_outgoing, thread_state.clone(), thread_watch_manager.clone(), - thread_list_state_permit.clone(), - fallback_model_provider.clone(), ) .await; if matches!(event.msg, EventMsg::ShutdownComplete) diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index f694d03c3b..0b5f502bd8 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -30,9 +30,6 @@ use std::ops::ControlFlow; pub(super) const THREAD_LIST_DEFAULT_LIMIT: usize = 25; pub(super) const THREAD_LIST_MAX_LIMIT: usize = 100; -const CODEX_TUI_CLIENT_NAME: &str = "codex-tui"; -const THREAD_ROLLBACK_DEPRECATION_SUMMARY: &str = - "thread/rollback is deprecated and will be removed soon"; const PAGINATED_FULL_HISTORY_DEPRECATION_SUMMARY: &str = "Full-history hydration is deprecated for paginated threads; use `excludeTurns: true`, then page with `thread/turns/list` and `thread/items/list`."; const PAGINATED_THREAD_READ_DEPRECATION_SUMMARY: &str = "Full-history hydration is deprecated for paginated threads; omit `includeTurns` or set it to `false`, then page with `thread/turns/list` and `thread/items/list`."; @@ -808,24 +805,6 @@ impl ThreadRequestProcessor { .map(|response| Some(response.into())) } - pub(crate) async fn thread_rollback( - &self, - request_id: &ConnectionRequestId, - params: ThreadRollbackParams, - app_server_client_name: Option<&str>, - ) -> Result, JSONRPCErrorError> { - if app_server_client_name != Some(CODEX_TUI_CLIENT_NAME) { - self.send_deprecation_notice( - request_id.connection_id, - THREAD_ROLLBACK_DEPRECATION_SUMMARY, - ) - .await; - } - self.thread_rollback_inner(request_id, params) - .await - .map(|()| None) - } - async fn send_deprecation_notice(&self, connection_id: ConnectionId, summary: &str) { self.outgoing .send_server_notification_to_connections( @@ -1102,8 +1081,6 @@ impl ThreadRequestProcessor { outgoing: Arc::clone(&self.outgoing), pending_thread_unloads: Arc::clone(&self.pending_thread_unloads), thread_watch_manager: self.thread_watch_manager.clone(), - thread_list_state_permit: self.thread_list_state_permit.clone(), - fallback_model_provider: self.config.model_provider_id.clone(), codex_home: self.config.codex_home.to_path_buf(), thread_unload_delay: self.config.thread_unload_delay, skills_watcher: Arc::clone(&self.skills_watcher), @@ -1234,8 +1211,6 @@ impl ThreadRequestProcessor { outgoing: Arc::clone(&self.outgoing), pending_thread_unloads: Arc::clone(&self.pending_thread_unloads), thread_watch_manager: self.thread_watch_manager.clone(), - thread_list_state_permit: self.thread_list_state_permit.clone(), - fallback_model_provider: self.config.model_provider_id.clone(), codex_home: self.config.codex_home.to_path_buf(), thread_unload_delay: self.config.thread_unload_delay, skills_watcher: Arc::clone(&self.skills_watcher), @@ -2118,14 +2093,6 @@ impl ThreadRequestProcessor { Ok((ThreadUnarchiveResponse { thread }, thread_id)) } - async fn thread_rollback_inner( - &self, - request_id: &ConnectionRequestId, - params: ThreadRollbackParams, - ) -> Result<(), JSONRPCErrorError> { - self.thread_rollback_start(request_id, params).await - } - async fn thread_revert_response( &self, request_id: &ConnectionRequestId, @@ -2353,71 +2320,6 @@ impl ThreadRequestProcessor { }) } - async fn thread_rollback_start( - &self, - request_id: &ConnectionRequestId, - params: ThreadRollbackParams, - ) -> Result<(), JSONRPCErrorError> { - let ThreadRollbackParams { - thread_id, - num_turns, - } = params; - - if num_turns == 0 { - return Err(invalid_request("numTurns must be >= 1")); - } - - let (thread_id, thread) = self.load_thread(&thread_id).await?; - ensure_direct_input_allowed(thread.as_ref()).await?; - if matches!( - thread.config_snapshot().await.history_mode, - ThreadHistoryMode::Paginated - ) { - return Err(invalid_request( - "paginated threads do not support thread/rollback", - )); - } - - let request = request_id.clone(); - let (completion_tx, completion_rx) = tokio::sync::oneshot::channel(); - - let rollback_already_in_progress = { - let thread_state = self.thread_state_manager.thread_state(thread_id).await; - let mut thread_state = thread_state.lock().await; - if thread_state.pending_rollbacks.is_some() { - true - } else { - thread_state.pending_rollbacks = Some((request, completion_tx)); - false - } - }; - if rollback_already_in_progress { - return Err(invalid_request( - "rollback already in progress for this thread", - )); - } - - if let Err(err) = self - .submit_core_op( - request_id, - thread.as_ref(), - Op::ThreadRollback { num_turns }, - ) - .await - { - // No ThreadRollback event will arrive if an error occurs. - // Clean up and reply immediately. - let thread_state = self.thread_state_manager.thread_state(thread_id).await; - thread_state.lock().await.pending_rollbacks = None; - - return Err(internal_error(format!("failed to start rollback: {err}"))); - } - // The listener drops the sender after queuing the response, including errors. - // Keep the RPC's drain admission alive until then, without holding thread state. - let _ = completion_rx.await; - Ok(()) - } - async fn thread_compact_start_inner( &self, request_id: &ConnectionRequestId, diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index f2a84fe623..78232498a5 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -85,7 +85,6 @@ pub(crate) struct TurnRequestProcessor { pending_thread_unloads: Arc>>, thread_state_manager: ThreadStateManager, thread_watch_manager: ThreadWatchManager, - thread_list_state_permit: Arc, skills_watcher: Arc, turn_cost_worker: Option, } @@ -149,7 +148,6 @@ impl TurnRequestProcessor { pending_thread_unloads: Arc>>, thread_state_manager: ThreadStateManager, thread_watch_manager: ThreadWatchManager, - thread_list_state_permit: Arc, skills_watcher: Arc, turn_cost_worker: Option, ) -> Self { @@ -166,7 +164,6 @@ impl TurnRequestProcessor { pending_thread_unloads, thread_state_manager, thread_watch_manager, - thread_list_state_permit, skills_watcher, turn_cost_worker, } @@ -1657,8 +1654,6 @@ impl TurnRequestProcessor { outgoing: Arc::clone(&self.outgoing), pending_thread_unloads: Arc::clone(&self.pending_thread_unloads), thread_watch_manager: self.thread_watch_manager.clone(), - thread_list_state_permit: self.thread_list_state_permit.clone(), - fallback_model_provider: self.config.model_provider_id.clone(), codex_home: self.config.codex_home.to_path_buf(), thread_unload_delay: self.config.thread_unload_delay, skills_watcher: Arc::clone(&self.skills_watcher), diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index dcba292310..14012f3f66 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -98,7 +98,6 @@ pub(crate) struct TurnSummary { #[derive(Default)] pub(crate) struct ThreadState { pub(crate) pending_interrupts: PendingInterruptQueue, - pub(crate) pending_rollbacks: Option<(ConnectionRequestId, oneshot::Sender<()>)>, pub(crate) turn_summary: TurnSummary, pub(crate) last_terminal_turn_id: Option, /// Lets an internal runtime replacement wait until the old listener has processed Core's @@ -146,7 +145,6 @@ impl ThreadState { let _ = cancel_tx.send(()); } self.shutdown_drain_waiter = None; - self.pending_rollbacks = None; self.listener_command_tx = None; self.current_turn_history.reset(); self.listener_thread = None; @@ -251,28 +249,6 @@ mod tests { use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; - #[test] - fn clear_listener_releases_pending_rollback() { - let (completion_tx, mut completion_rx) = oneshot::channel(); - let mut state = ThreadState { - pending_rollbacks: Some(( - ConnectionRequestId { - connection_id: ConnectionId(1), - request_id: RequestId::Integer(1), - }, - completion_tx, - )), - ..Default::default() - }; - - state.clear_listener(); - - assert_eq!( - completion_rx.try_recv(), - Err(oneshot::error::TryRecvError::Closed) - ); - } - #[test] fn note_thread_settings_reports_only_effective_changes() { let mut state = ThreadState::default(); diff --git a/codex-rs/app-server/tests/common/test_app_server.rs b/codex-rs/app-server/tests/common/test_app_server.rs index 568916e93b..8a0a2fefe3 100644 --- a/codex-rs/app-server/tests/common/test_app_server.rs +++ b/codex-rs/app-server/tests/common/test_app_server.rs @@ -104,7 +104,6 @@ use codex_app_server_protocol::ThreadRealtimeListVoicesParams; use codex_app_server_protocol::ThreadRealtimeStartParams; use codex_app_server_protocol::ThreadRealtimeStopParams; use codex_app_server_protocol::ThreadResumeParams; -use codex_app_server_protocol::ThreadRollbackParams; use codex_app_server_protocol::ThreadSearchOccurrencesParams; use codex_app_server_protocol::ThreadSearchParams; use codex_app_server_protocol::ThreadSectionMoveParams; @@ -659,15 +658,6 @@ impl TestAppServer { self.send_request("thread/shellCommand", params).await } - /// Send a `thread/rollback` JSON-RPC request. - pub async fn send_thread_rollback_request( - &mut self, - params: ThreadRollbackParams, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(params)?); - self.send_request("thread/rollback", params).await - } - /// Send a `thread/list` JSON-RPC request. pub async fn send_thread_list_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket_unix.rs b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket_unix.rs index 0575a4e1f2..4bc3ab9993 100644 --- a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket_unix.rs +++ b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket_unix.rs @@ -97,10 +97,6 @@ async fn websocket_transport_ctrl_c_waits_for_running_turn_before_exit() -> Resu "turn/settings/update", json!({"threadId": thread_id, "turnId": "unused", "model": "other"}), ), - ( - "thread/rollback", - json!({"threadId": thread_id, "numTurns": 1}), - ), ( "thread/revert", json!({"threadId": thread_id, "beforeTurnId": "unused"}), diff --git a/codex-rs/app-server/tests/suite/v2/git_attribution.rs b/codex-rs/app-server/tests/suite/v2/git_attribution.rs index 5925dd3d4f..87a9c824e9 100644 --- a/codex-rs/app-server/tests/suite/v2/git_attribution.rs +++ b/codex-rs/app-server/tests/suite/v2/git_attribution.rs @@ -17,8 +17,6 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadHistoryMode; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; -use codex_app_server_protocol::ThreadRollbackParams; -use codex_app_server_protocol::ThreadRollbackResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -79,7 +77,7 @@ async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()> "Recovered", "Cached", "After switch", - "After rollback", + "After switching back", ] .into_iter() .map(create_final_assistant_message_sse_response) @@ -166,14 +164,6 @@ async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()> let _: LoginAccountResponse = read_response(&mut app_server, request_id).await?; run_turn(&mut app_server, &thread.id, "Turn after workspace switch").await?; - let request_id = app_server - .send_thread_rollback_request(ThreadRollbackParams { - thread_id: thread.id.clone(), - num_turns: 1, - }) - .await?; - let _: ThreadRollbackResponse = read_response(&mut app_server, request_id).await?; - let request_id = app_server .send_chatgpt_auth_tokens_login_request( "e30.e30.c2ln".to_string(), @@ -182,13 +172,13 @@ async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()> ) .await?; let _: LoginAccountResponse = read_response(&mut app_server, request_id).await?; - run_turn(&mut app_server, &thread.id, "Turn after rollback").await?; + run_turn(&mut app_server, &thread.id, "Turn after switching back").await?; let requests = response_mock.requests(); assert_eq!(requests.len(), 5); for (request, expected) in requests .into_iter() - .zip([(0, 0), (1, 0), (1, 0), (1, 1), (1, 0)]) + .zip([(0, 0), (1, 0), (1, 0), (1, 1), (2, 1)]) { let developer_text = request.message_input_texts("developer").join("\n"); assert_eq!( diff --git a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs index ea22b528a2..bc2d3685ac 100644 --- a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs +++ b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs @@ -36,12 +36,9 @@ use codex_app_server_protocol::ThreadCompactStartParams; use codex_app_server_protocol::ThreadCompactStartResponse; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; -use codex_app_server_protocol::ThreadHistoryMode; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; -use codex_app_server_protocol::ThreadRollbackParams; -use codex_app_server_protocol::ThreadRollbackResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnSettingsUpdateParams; @@ -247,7 +244,6 @@ enum ThreadLifecycle { UserInputHookBlocked, Resume, Fork, - RootRollback, RootRestriction, RootRestrictionDuringClassification, RootTrustedSkill, @@ -261,8 +257,7 @@ impl ThreadLifecycle { fn uses_root_worker(self) -> bool { matches!( self, - Self::RootRollback - | Self::RootRestriction + Self::RootRestriction | Self::RootRestrictionDuringClassification | Self::RootTrustedSkill | Self::RootUserInputRestriction @@ -914,7 +909,6 @@ async fn guardian_v2_routes_scoped_tool_approvals( | ThreadLifecycle::UserInputEmpty | ThreadLifecycle::UserInputHookFeedback | ThreadLifecycle::UserInputHookBlocked - | ThreadLifecycle::RootRollback | ThreadLifecycle::RootRestriction | ThreadLifecycle::RootRestrictionDuringClassification | ThreadLifecycle::RootTrustedSkill @@ -963,7 +957,6 @@ async fn guardian_v2_routes_scoped_tool_approvals( | ThreadLifecycle::UserInputEmpty | ThreadLifecycle::UserInputHookFeedback | ThreadLifecycle::UserInputHookBlocked - | ThreadLifecycle::RootRollback | ThreadLifecycle::RootRestriction | ThreadLifecycle::RootRestrictionDuringClassification | ThreadLifecycle::RootTrustedSkill @@ -975,8 +968,6 @@ async fn guardian_v2_routes_scoped_tool_approvals( .start_thread(ThreadStartParams { approval_policy: Some(AskForApproval::OnRequest), approvals_reviewer: Some(requested_reviewer), - history_mode: matches!(lifecycle, ThreadLifecycle::RootRollback) - .then_some(ThreadHistoryMode::Legacy), ..Default::default() }) .await?; @@ -1560,17 +1551,6 @@ async fn guardian_v2_routes_scoped_tool_approvals( && (lifecycle.uses_root_worker() || matches!(lifecycle, ThreadLifecycle::RootUserRestriction)) { - if matches!(lifecycle, ThreadLifecycle::RootRollback) { - let rollback_id = app_server - .send_thread_rollback_request(ThreadRollbackParams { - thread_id: thread_id.clone(), - num_turns: 1, - }) - .await?; - let _: ThreadRollbackResponse = - timeout(TIMEOUT, app_server.read_response(rollback_id)).await??; - } - if lifecycle.has_root_user_input() { submit_user_input_response( &mut app_server, @@ -2548,7 +2528,6 @@ async fn forked_thread_ignores_persisted_guardian_score() -> Result<()> { .await } -#[test_case(ThreadLifecycle::RootRollback; "worker_root_rollback")] #[test_case(ThreadLifecycle::RootRestriction; "worker_root_restriction")] #[test_case(ThreadLifecycle::RootUserRestriction; "root_user_restriction")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/codex-rs/app-server/tests/suite/v2/guardian_v2_history_tests.rs b/codex-rs/app-server/tests/suite/v2/guardian_v2_history_tests.rs index ca1454f1d8..6f934fab44 100644 --- a/codex-rs/app-server/tests/suite/v2/guardian_v2_history_tests.rs +++ b/codex-rs/app-server/tests/suite/v2/guardian_v2_history_tests.rs @@ -1,4 +1,4 @@ -//! Exercises both reviewers' evidence delivery through real compaction and rollback. +//! Exercises both reviewers' evidence delivery through real compaction and resume. use std::sync::Arc; use std::sync::Mutex; @@ -24,8 +24,6 @@ use codex_app_server_protocol::ThreadCompactStartResponse; use codex_app_server_protocol::ThreadHistoryMode; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; -use codex_app_server_protocol::ThreadRollbackParams; -use codex_app_server_protocol::ThreadRollbackResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnStartParams; @@ -108,7 +106,7 @@ enum ReviewCheckpoint { #[test_case(ContextPath::Legacy, CheckpointReuse::Enabled, Some("matching"), Some("matching"), 140, EvidenceSize::Normal, ReviewCheckpoint::Valid; "legacy source call evicted")] #[test_case(ContextPath::Legacy, CheckpointReuse::Enabled, Some("matching"), Some("matching"), 0, EvidenceSize::OversizedAnswer, ReviewCheckpoint::Valid; "legacy answer truncation")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollback( +async fn guardians_retain_evidence_after_compaction_and_resume( context_path: ContextPath, checkpoint_reuse: CheckpointReuse, parent_hash: Option<&str>, @@ -334,8 +332,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac restriction.as_str(), "Recheck the repository.", "Inspect after resume.", - "Inspect after partial rollback.", - "Inspect a different repository.", ] .into_iter() .enumerate() @@ -368,15 +364,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac timeout(TIMEOUT, app_server.read_notification("turn/completed")).await??; assert_eq!(completed.turn.status, TurnStatus::Completed); assert_eq!(compact_requests.lock().expect("request log lock").len(), 1); - } else if index >= 3 { - let id = app_server - .send_thread_rollback_request(ThreadRollbackParams { - thread_id: thread_id.clone(), - num_turns: if index == 3 { 1 } else { 3 }, - }) - .await?; - let _: ThreadRollbackResponse = - timeout(TIMEOUT, app_server.read_response(id)).await??; } let id = app_server @@ -492,7 +479,7 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac } continue; } - if requires_sync && (1..=3).contains(&index) { + if requires_sync && index > 0 { let reviews = review_requests.lock().expect("request log lock"); assert_eq!( reviews.len(), @@ -513,12 +500,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac "current user input missing from sync review: {text}" ); } - if index == 3 { - assert!( - !text.contains("Inspect after resume."), - "rolled-back user input remains in sync review: {text}" - ); - } assert!(text.contains(USER_INPUT_RESTRICTION)); assert!(text.contains("TRUSTED USER ANSWERS START")); assert!(text.contains("TRANSCRIPT START")); @@ -575,11 +556,8 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac let review = &reviews[index]; let sync_input = review["input"].as_array().expect("request input array"); let async_input = request["input"].as_array().expect("request input array"); - assert_eq!(sync_input.contains(&checkpoint), (1..=3).contains(&index)); - assert_eq!( - async_input.contains(&checkpoint), - (1..=3).contains(&index) && compatible - ); + assert_eq!(sync_input.contains(&checkpoint), index > 0); + assert_eq!(async_input.contains(&checkpoint), index > 0 && compatible); let sync_text = sync_input .iter() .filter(|item| item["role"] == "user") @@ -591,14 +569,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac sync_text.contains(prompt), "current user input missing from sync review: {sync_text}" ); - if index == 3 { - for (consumer, text) in [("sync", sync_text.as_str()), ("async", transcript)] { - assert!( - !text.contains("Inspect after resume."), - "rolled-back user input remains in {consumer} review: {text}" - ); - } - } if index == 0 { let input = parent[2]["input"].as_array().expect("request input array"); let output = input @@ -615,12 +585,12 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac .expect("request input array"); let parent_input = serde_json::to_string(parent_items)?; // V2 keeps bounded user history while replacing old tool output with a checkpoint. - assert_eq!(parent_input.contains(RESTRICTION), index <= 3); + assert!(parent_input.contains(RESTRICTION)); assert!(!parent_input.contains(EVIDENCE)); assert!(!parent_items.iter().any(|item| { item["type"] == "function_call_output" && item["call_id"] == "inspect-1" })); - if index <= 3 { + { assert!(sync_text.contains(RESTRICTION)); if matches!(context_path, ContextPath::ThreadOwned) { assert!( @@ -678,17 +648,10 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac assert!(!transcript.contains("tool request_user_input call:")); } } - } else { - assert!(!sync_text.contains(RESTRICTION)); - assert!(!sync_text.contains(EVIDENCE)); - assert!(!transcript.contains(RESTRICTION)); - assert!(!transcript.contains(EVIDENCE)); - assert!(!transcript.contains("Recheck the repository.")); - assert!(!transcript.contains("\"echoed\":\"current inspection\"")); } } for (consumer, text) in [("async", &content), ("sync", &sync_text)] { - if index < 4 && (matches!(context_path, ContextPath::ThreadOwned) || index == 0) { + if matches!(context_path, ContextPath::ThreadOwned) || index == 0 { let answers = text .split_once(">>> TRUSTED USER ANSWERS START") .unwrap_or_else(|| { @@ -715,9 +678,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac }, } } else { - if index == 4 { - assert!(!text.contains(USER_INPUT_RESTRICTION)); - } assert!(!text.contains(">>> TRUSTED USER ANSWERS START")); } } diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 0c79256813..d545561e74 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -126,7 +126,6 @@ mod thread_queue; mod thread_read; mod thread_resume; mod thread_revert; -mod thread_rollback; mod thread_sections; mod thread_settings_update; mod thread_shell_command; diff --git a/codex-rs/app-server/tests/suite/v2/thread_rollback.rs b/codex-rs/app-server/tests/suite/v2/thread_rollback.rs deleted file mode 100644 index 48c48d8ed1..0000000000 --- a/codex-rs/app-server/tests/suite/v2/thread_rollback.rs +++ /dev/null @@ -1,343 +0,0 @@ -use anyhow::Result; -use app_test_support::MockResponsesConfig; -use app_test_support::TestAppServer; -use app_test_support::create_final_assistant_message_sse_response; -use app_test_support::create_mock_responses_server_repeating_assistant; -use app_test_support::create_mock_responses_server_sequence_unchecked; -use app_test_support::to_response; -use codex_app_server_protocol::ClientInfo; -use codex_app_server_protocol::DeprecationNoticeNotification; -use codex_app_server_protocol::JSONRPCError; -use codex_app_server_protocol::JSONRPCMessage; -use codex_app_server_protocol::JSONRPCResponse; -use codex_app_server_protocol::RequestId; -use codex_app_server_protocol::ThreadHistoryMode; -use codex_app_server_protocol::ThreadItem; -use codex_app_server_protocol::ThreadResumeParams; -use codex_app_server_protocol::ThreadResumeResponse; -use codex_app_server_protocol::ThreadRollbackParams; -use codex_app_server_protocol::ThreadRollbackResponse; -use codex_app_server_protocol::ThreadStartParams; -use codex_app_server_protocol::ThreadStartResponse; -use codex_app_server_protocol::ThreadStatus; -use codex_app_server_protocol::TurnStartParams; -use codex_app_server_protocol::UserInput as V2UserInput; -use codex_protocol::openai_models::ReasoningEffort; -use pretty_assertions::assert_eq; -use serde_json::Value; -use serde_json::json; -use tempfile::TempDir; -use tokio::time::timeout; - -const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); - -#[tokio::test] -async fn thread_rollback_rejects_paginated_thread() -> 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 mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .without_auto_env() - .build() - .await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - let start_id = mcp - .send_thread_start_request(ThreadStartParams { - history_mode: Some(ThreadHistoryMode::Paginated), - ..Default::default() - }) - .await?; - let start_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(start_id)), - ) - .await??; - let ThreadStartResponse { thread, .. } = to_response(start_resp)?; - - let rollback_id = mcp - .send_thread_rollback_request(ThreadRollbackParams { - thread_id: thread.id, - num_turns: 1, - }) - .await?; - let rollback_err: JSONRPCError = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_error_message(RequestId::Integer(rollback_id)), - ) - .await??; - assert_eq!(rollback_err.error.code, -32600); - assert_eq!( - rollback_err.error.message, - "paginated threads do not support thread/rollback" - ); - - Ok(()) -} - -#[tokio::test] -async fn thread_rollback_does_not_emit_deprecation_notice_to_codex_tui() -> Result<()> { - let codex_home = TempDir::new()?; - let mut mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .build() - .await?; - let initialized = timeout( - DEFAULT_READ_TIMEOUT, - mcp.initialize_with_client_info(ClientInfo { - name: "codex-tui".to_string(), - title: None, - version: "0.1.0".to_string(), - }), - ) - .await??; - let JSONRPCMessage::Response(_) = initialized else { - panic!("expected initialize response, got {initialized:?}"); - }; - mcp.clear_message_buffer(); - - let rollback_id = mcp - .send_thread_rollback_request(ThreadRollbackParams { - thread_id: "00000000-0000-0000-0000-000000000001".to_string(), - num_turns: 1, - }) - .await?; - loop { - let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??; - match message { - JSONRPCMessage::Notification(notification) => { - assert_ne!(notification.method, "deprecationNotice"); - } - JSONRPCMessage::Error(error) if error.id == RequestId::Integer(rollback_id) => { - break; - } - message => { - panic!("expected rollback error response, got {message:?}"); - } - } - } - - Ok(()) -} - -#[tokio::test] -async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<()> { - // Three Codex turns hit the mock model (session start + two turn/start calls). - let responses = vec![ - create_final_assistant_message_sse_response("Done")?, - create_final_assistant_message_sse_response("Done")?, - create_final_assistant_message_sse_response("Done")?, - ]; - let server = create_mock_responses_server_sequence_unchecked(responses).await; - - let codex_home = TempDir::new()?; - MockResponsesConfig::new(&server.uri()).write(codex_home.path())?; - - let mut mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .build() - .await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - - // Start a thread. - let start_id = mcp - .send_thread_start_request_with_auto_env(ThreadStartParams { - model: Some("mock-model".to_string()), - history_mode: Some(ThreadHistoryMode::Legacy), - ..Default::default() - }) - .await?; - let start_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(start_id)), - ) - .await??; - let ThreadStartResponse { thread, .. } = to_response::(start_resp)?; - - // Two turns. - let first_text = "First"; - let turn1_id = mcp - .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), - client_user_message_id: None, - input: vec![V2UserInput::Text { - text: first_text.to_string(), - text_elements: Vec::new(), - }], - ..Default::default() - }) - .await?; - let _turn1_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(turn1_id)), - ) - .await??; - let _completed1 = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - - let turn2_id = mcp - .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), - client_user_message_id: None, - input: vec![V2UserInput::Text { - text: "Second".to_string(), - text_elements: Vec::new(), - }], - ..Default::default() - }) - .await?; - let _turn2_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(turn2_id)), - ) - .await??; - let _completed2 = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - drop(mcp); - let mut mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .build_initialized() - .await?; - let resume_id = mcp - .send_thread_resume_request(ThreadResumeParams { - thread_id: thread.id.clone(), - model: Some("gpt-5.2".to_string()), - config: Some([("model_reasoning_effort".to_string(), json!("high"))].into()), - ..Default::default() - }) - .await?; - let ThreadResumeResponse { thread, .. } = - timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(resume_id)).await??; - // Drain the resume snapshots before checking rollback notification ordering. - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("thread/goal/cleared"), - ) - .await??; - mcp.clear_message_buffer(); - - // Roll back the last turn. - let rollback_id = mcp - .send_thread_rollback_request(ThreadRollbackParams { - thread_id: thread.id.clone(), - num_turns: 1, - }) - .await?; - // Pipeline resume while the listener may still be handling rollback. Waiting for - // its response must not retain the thread-list permit needed by that listener. - let pipelined_resume_id = mcp - .send_thread_resume_request(ThreadResumeParams { - thread_id: thread.id.clone(), - exclude_turns: true, - ..Default::default() - }) - .await?; - let deprecation_notice = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??; - let JSONRPCMessage::Notification(deprecation_notice) = deprecation_notice else { - panic!("thread/rollback should emit deprecationNotice before its response"); - }; - assert_eq!(deprecation_notice.method, "deprecationNotice"); - let deprecation_notice: DeprecationNoticeNotification = serde_json::from_value( - deprecation_notice - .params - .expect("deprecationNotice params should be present"), - )?; - assert_eq!( - deprecation_notice, - DeprecationNoticeNotification { - summary: "thread/rollback is deprecated and will be removed soon".to_string(), - details: None, - } - ); - let rollback_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(rollback_id)), - ) - .await??; - let rollback_result = rollback_resp.result.clone(); - let ThreadRollbackResponse { - thread: rolled_back_thread, - } = to_response::(rollback_resp)?; - - // Wire contract: thread title field is `name`, serialized as null when unset. - let thread_json = rollback_result - .get("thread") - .and_then(Value::as_object) - .expect("thread/rollback result.thread must be an object"); - assert_eq!(rolled_back_thread.name, None); - assert_eq!(rolled_back_thread.session_id, thread.session_id); - assert_eq!( - thread_json.get("name"), - Some(&Value::Null), - "thread/rollback must serialize `name: null` when unset" - ); - assert_eq!( - thread_json.get("sessionId").and_then(Value::as_str), - Some(thread.session_id.as_str()) - ); - - assert_eq!(rolled_back_thread.turns.len(), 1); - assert_eq!(rolled_back_thread.status, ThreadStatus::Idle); - assert_eq!( - ( - rolled_back_thread.model.as_deref(), - rolled_back_thread.reasoning_effort - ), - (Some("gpt-5.2"), Some(ReasoningEffort::High)) - ); - assert_eq!(rolled_back_thread.turns[0].items.len(), 2); - match &rolled_back_thread.turns[0].items[0] { - ThreadItem::UserMessage { content, .. } => { - assert_eq!( - content, - &vec![V2UserInput::Text { - text: first_text.to_string(), - text_elements: Vec::new(), - }] - ); - } - other => panic!("expected user message item, got {other:?}"), - } - - let _: ThreadResumeResponse = - timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(pipelined_resume_id)).await??; - - // Resume after rollback completes to verify the pruned history. - let resume_id = mcp - .send_thread_resume_request(ThreadResumeParams { - thread_id: thread.id, - ..Default::default() - }) - .await?; - let resume_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(resume_id)), - ) - .await??; - let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?; - - assert_eq!(thread.turns.len(), 1); - assert_eq!(thread.status, ThreadStatus::Idle); - assert_eq!(thread.turns[0].items.len(), 2); - match &thread.turns[0].items[0] { - ThreadItem::UserMessage { content, .. } => { - assert_eq!( - content, - &vec![V2UserInput::Text { - text: first_text.to_string(), - text_elements: Vec::new(), - }] - ); - } - other => panic!("expected user message item, got {other:?}"), - } - - Ok(()) -} diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index c622f1a91f..a8881ddd7e 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -4594,7 +4594,6 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected( "turn/settings/update", json!({"turnId": "any-child-turn", "model": "gpt-5.4"}), ), - ("thread/rollback", json!({"numTurns": 1})), ("thread/revert", json!({"beforeTurnId": "any-child-turn"})), ( "review/start", diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 799b9eb6a7..f9ad849516 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -522,7 +522,7 @@ impl ContextManager { /// Instruction turns are history messages that should behave like a new prompt boundary: /// ordinary user messages and structured assistant inter-agent instructions. /// - /// This mirrors thread-rollback semantics: + /// Used only to replay historical rollback markers when reconstructing a saved rollout: /// - `num_turns == 0` is a no-op /// - if there are no user turns, this is a no-op /// - if `num_turns` exceeds the number of user turns, all user turns are dropped while diff --git a/codex-rs/core/src/rollout_budget.rs b/codex-rs/core/src/rollout_budget.rs index cb4b9f8bd4..d66b778fa8 100644 --- a/codex-rs/core/src/rollout_budget.rs +++ b/codex-rs/core/src/rollout_budget.rs @@ -109,14 +109,6 @@ impl RolloutBudget { ); } - /// Forces the next sampling request for `thread_id` to restate the current remainder. - pub(crate) fn rearm_reminder(&self, thread_id: ThreadId) { - let Some(mut state) = self.lock() else { - return; - }; - state.deliveries.remove(&thread_id); - } - fn lock(&self) -> Option> { self.state.get().map(|state| { state diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index cf173b4688..8c43908eee 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -17,15 +17,12 @@ use crate::session::turn_input; use crate::config::Config; use crate::context::ContextualUserFragment; use crate::context::GuardianApprovedAction; -use crate::context::NodeReplReviewEvidence; use crate::review_prompts::resolve_review_request; use crate::session::spawn_review_thread; -use crate::state::ReasoningEffortPin; use crate::tasks::CompactTask; use crate::tasks::UserShellCommandMode; use crate::tasks::UserShellCommandTask; use crate::tasks::execute_user_shell_command; -use codex_history::RolloutItem; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::Event; @@ -39,7 +36,6 @@ use codex_protocol::protocol::RealtimeVoicesList; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ThreadMemoryMode; -use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::WarningEvent; use codex_protocol::request_permissions::RequestPermissionsResponse; @@ -252,129 +248,6 @@ pub async fn compact(sess: &Arc, sub_id: String) { sess.spawn_task(turn_context, Vec::new(), CompactTask).await; } -pub async fn thread_rollback(sess: &Arc, sub_id: String, num_turns: u32) { - if num_turns == 0 { - sess.send_event_raw(Event { - id: sub_id, - msg: EventMsg::Error(ErrorEvent { - misalignment: None, - message: "num_turns must be >= 1".to_string(), - codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), - }), - }) - .await; - return; - } - - let has_active_turn = { sess.active_turn.lock().await.is_some() }; - if has_active_turn { - sess.send_event_raw(Event { - id: sub_id, - msg: EventMsg::Error(ErrorEvent { - misalignment: None, - message: "Cannot rollback while a turn is in progress.".to_string(), - codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), - }), - }) - .await; - return; - } - - let turn_context = sess - .new_turn_with_default_settings(sub_id, Default::default()) - .await; - let live_thread = match sess.live_thread_for_persistence("rollback thread") { - Ok(live_thread) => live_thread, - Err(_) => { - sess.send_event_raw(Event { - id: turn_context.sub_id.clone(), - msg: EventMsg::Error(ErrorEvent { - misalignment: None, - message: "thread rollback requires persisted thread history".to_string(), - codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), - }), - }) - .await; - return; - } - }; - if let Err(err) = live_thread.flush().await { - sess.send_event_raw(Event { - id: turn_context.sub_id.clone(), - msg: EventMsg::Error(ErrorEvent { - misalignment: None, - message: format!("failed to flush thread persistence for rollback replay: {err}"), - codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), - }), - }) - .await; - return; - } - - let stored_history = match live_thread.load_history(/*include_archived*/ false).await { - Ok(history) => history, - Err(err) => { - sess.send_event_raw(Event { - id: turn_context.sub_id.clone(), - msg: EventMsg::Error(ErrorEvent { - misalignment: None, - message: format!("failed to load thread history for rollback replay: {err}"), - codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), - }), - }) - .await; - return; - } - }; - - let rollback_event = ThreadRolledBackEvent { num_turns }; - let rollback_msg = EventMsg::ThreadRolledBack(rollback_event.clone()); - let replay_items = stored_history - .items - .into_iter() - .chain(std::iter::once(RolloutItem::EventMsg(rollback_msg.clone()))) - .collect::>(); - sess.apply_rollout_reconstruction(turn_context.as_ref(), replay_items.as_slice()) - .await; - { - let mut state = sess.state.lock().await; - // Keep the baseline while startup prewarm is retained for the first turn, - // including when its task has not established the pin yet. - if state.startup_prewarm.is_none() { - state.reasoning_effort_pin = ReasoningEffortPin::Unset; - } - } - sess.services - .thread_extension_data - .remove::(); - sess.guardian_review_session().invalidate().await; - sess.services - .agent_control - .rollout_budget() - .rearm_reminder(sess.thread_id()); - sess.recompute_token_usage(turn_context.as_ref()).await; - - sess.persist_rollout_items(&[RolloutItem::EventMsg(rollback_msg.clone())]) - .await; - if let Err(err) = sess.flush_rollout().await { - sess.send_event( - turn_context.as_ref(), - EventMsg::Warning(WarningEvent { - message: format!( - "Rolled the thread back, but failed to save the rollback marker. Codex will continue retrying. Error: {err}" - ), - }), - ) - .await; - } - - sess.deliver_event_raw(Event { - id: turn_context.sub_id.clone(), - msg: rollback_msg, - }) - .await; -} - pub(super) async fn persist_thread_memory_mode_update( sess: &Arc, mode: ThreadMemoryMode, @@ -689,10 +562,6 @@ pub(super) async fn submission_loop( compact(&sess, sub.id.clone()).await; false } - Op::ThreadRollback { num_turns } => { - thread_rollback(&sess, sub.id.clone(), num_turns).await; - false - } Op::SetThreadMemoryMode { mode } => { set_thread_memory_mode(&sess, sub.id.clone(), mode).await; false diff --git a/codex-rs/core/src/session/reasoning_effort.rs b/codex-rs/core/src/session/reasoning_effort.rs index 5529a0d43f..5ff84dc2d1 100644 --- a/codex-rs/core/src/session/reasoning_effort.rs +++ b/codex-rs/core/src/session/reasoning_effort.rs @@ -1,7 +1,7 @@ //! Cache-preserving effort updates and the request-effort baseline for a context window. //! -//! Only trusted harness items establish overrides. Replay and rollback preserve startup -//! prewarm's baseline while it is retained; later rollback invalidates the runtime pin. +//! Only trusted harness items establish overrides. Replay preserves startup prewarm's +//! baseline while it is retained. //! Successful compaction retires the overrides and allows a fresh request baseline. use super::session::Session; diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 56a00c0d61..6abb81d892 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -86,7 +86,6 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSandboxPolicyContext; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::EnvironmentConfigState; -use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::request_permissions::PermissionGrantScope; @@ -165,7 +164,6 @@ use codex_protocol::protocol::RealtimeVoicesList; use codex_protocol::protocol::SessionMeta; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::Submission; -use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::TokenCountEvent; use codex_protocol::protocol::TokenUsage; @@ -3906,608 +3904,6 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { ); } -#[tokio::test] -async fn thread_rollback_drops_last_turn_from_history() { - let (mut sess, tc, rx) = make_session_and_context_with_rx().await; - let rollout_path = attach_thread_persistence( - Arc::get_mut(&mut sess).expect("session should not have additional references"), - ) - .await; - - let initial_context = build_initial_context(&sess, &tc).await; - let turn_1 = vec![ - user_message("turn 1 user"), - assistant_message("turn 1 assistant"), - ]; - let turn_2 = vec![ - user_message("turn 2 user"), - assistant_message("turn 2 assistant"), - ]; - let mut full_history = Vec::new(); - full_history.extend(initial_context.clone()); - full_history.extend(turn_1.clone()); - full_history.extend(turn_2); - sess.replace_history(full_history.clone(), Some(tc.to_turn_context_item())) - .await; - let rollout_items: Vec = full_history - .into_iter() - .map(ResponseItemEnvelope::new) - .map(RolloutItem::ResponseItem) - .collect(); - sess.persist_rollout_items(&rollout_items).await; - sess.set_previous_turn_settings(Some(PreviousTurnSettings { - model: "stale-model".to_string(), - comp_hash: None, - realtime_active: Some(tc.realtime_active), - })) - .await; - { - let mut state = sess.state.lock().await; - state.set_reference_context_item(Some(tc.to_turn_context_item())); - } - - handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; - - let rollback_event = wait_for_thread_rolled_back(&rx).await; - assert_eq!(rollback_event.num_turns, 1); - - let mut expected = Vec::new(); - expected.extend(initial_context); - expected.extend(turn_1); - - let history = sess.clone_history().await; - assert_eq!(expected, raw_history_items(&history)); - assert_eq!(sess.previous_turn_settings().await, None); - assert!(sess.reference_context_item().await.is_none()); - - let InitialHistory::Resumed(resumed) = RolloutRecorder::get_rollout_history(&rollout_path) - .await - .expect("read rollout history") - else { - panic!("expected resumed rollout history"); - }; - assert!(resumed.history.iter().any(|item| { - matches!( - item, - RolloutItem::EventMsg(EventMsg::ThreadRolledBack(rollback)) - if rollback.num_turns == 1 - ) - })); -} - -#[tokio::test] -async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns() { - let (mut sess, tc, rx) = make_session_and_context_with_rx().await; - attach_thread_persistence( - Arc::get_mut(&mut sess).expect("session should not have additional references"), - ) - .await; - - let initial_context = build_initial_context(&sess, &tc).await; - let turn_1 = vec![user_message("turn 1 user")]; - let mut full_history = Vec::new(); - full_history.extend(initial_context.clone()); - full_history.extend(turn_1); - sess.replace_history(full_history.clone(), Some(tc.to_turn_context_item())) - .await; - let rollout_items: Vec = full_history - .into_iter() - .map(ResponseItemEnvelope::new) - .map(RolloutItem::ResponseItem) - .collect(); - sess.persist_rollout_items(&rollout_items).await; - - handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 99).await; - - let rollback_event = wait_for_thread_rolled_back(&rx).await; - assert_eq!(rollback_event.num_turns, 99); - - let history = sess.clone_history().await; - assert_eq!(initial_context, raw_history_items(&history)); -} - -#[tokio::test] -async fn thread_rollback_fails_without_persisted_thread_history() { - let (sess, tc, rx) = make_session_and_context_with_rx().await; - - let initial_context = build_initial_context(&sess, &tc).await; - sess.record_conversation_items(tc.as_ref(), tc.model_info(), &initial_context) - .await; - let history_before_rollback = sess.clone_history().await; - - handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; - - let error_event = wait_for_thread_rollback_failed(&rx).await; - assert_eq!( - error_event.message, - "thread rollback requires persisted thread history" - ); - assert_eq!( - error_event.codex_error_info, - Some(CodexErrorInfo::ThreadRollbackFailed) - ); - assert_eq!( - raw_history_items(&sess.clone_history().await), - raw_history_items(&history_before_rollback) - ); -} - -#[tokio::test] -async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context_from_replay() { - let (mut sess, tc, rx) = make_session_and_context_with_rx().await; - attach_thread_persistence( - Arc::get_mut(&mut sess).expect("session should not have additional references"), - ) - .await; - - let first_context_item = tc.to_turn_context_item(); - let first_turn_id = first_context_item - .turn_id - .clone() - .expect("thread settings should have turn_id"); - let mut rolled_back_context_item = first_context_item.clone(); - rolled_back_context_item.turn_id = Some("rolled-back-turn".to_string()); - rolled_back_context_item.model = "rolled-back-model".to_string(); - let rolled_back_turn_id = rolled_back_context_item - .turn_id - .clone() - .expect("thread settings should have turn_id"); - let turn_one_user = user_message("turn 1 user"); - let turn_one_assistant = assistant_message("turn 1 assistant"); - let turn_two_user = user_message("turn 2 user"); - let turn_two_assistant = assistant_message("turn 2 assistant"); - - sess.persist_rollout_items(&[ - RolloutItem::EventMsg(EventMsg::TurnStarted( - codex_protocol::protocol::TurnStartedEvent { - turn_id: first_turn_id.clone(), - root_turn_id: None, - trace_id: None, - started_at: None, - model_context_window: Some(128_000), - collaboration_mode_kind: ModeKind::Default, - }, - )), - RolloutItem::EventMsg(EventMsg::UserMessage( - codex_protocol::protocol::UserMessageEvent { - client_id: None, - message: "turn 1 user".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - }, - )), - RolloutItem::TurnContext(first_context_item.clone()), - RolloutItem::ResponseItem(turn_one_user.clone().into()), - RolloutItem::ResponseItem(turn_one_assistant.clone().into()), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: first_turn_id, - started_at: None, - last_agent_message: None, - error: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })), - RolloutItem::EventMsg(EventMsg::TurnStarted( - codex_protocol::protocol::TurnStartedEvent { - turn_id: rolled_back_turn_id.clone(), - root_turn_id: None, - trace_id: None, - started_at: None, - model_context_window: Some(128_000), - collaboration_mode_kind: ModeKind::Default, - }, - )), - RolloutItem::EventMsg(EventMsg::UserMessage( - codex_protocol::protocol::UserMessageEvent { - client_id: None, - message: "turn 2 user".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - }, - )), - RolloutItem::TurnContext(rolled_back_context_item), - RolloutItem::ResponseItem(turn_two_user.into()), - RolloutItem::ResponseItem(turn_two_assistant.into()), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: rolled_back_turn_id, - started_at: None, - last_agent_message: None, - error: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })), - ]) - .await; - sess.replace_history( - vec![assistant_message("stale history")], - Some(first_context_item.clone()), - ) - .await; - sess.set_previous_turn_settings(Some(PreviousTurnSettings { - model: "stale-model".to_string(), - comp_hash: None, - realtime_active: None, - })) - .await; - - handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; - let rollback_event = wait_for_thread_rolled_back(&rx).await; - assert_eq!(rollback_event.num_turns, 1); - - assert_eq!( - raw_history_items(&sess.clone_history().await), - vec![turn_one_user, turn_one_assistant] - ); - assert_eq!( - sess.previous_turn_settings().await, - Some(PreviousTurnSettings { - model: tc.model_info().slug.clone(), - comp_hash: None, - realtime_active: Some(tc.realtime_active), - }) - ); - assert_eq!( - serde_json::to_value(sess.reference_context_item().await) - .expect("serialize replay reference context item"), - serde_json::to_value(Some(first_context_item)) - .expect("serialize expected reference context item") - ); -} - -#[tokio::test] -async fn thread_rollback_restores_cleared_reference_context_item_after_compaction() { - let (mut sess, tc, rx) = make_session_and_context_with_rx().await; - attach_thread_persistence( - Arc::get_mut(&mut sess).expect("session should not have additional references"), - ) - .await; - - let first_context_item = tc.to_turn_context_item(); - let first_turn_id = first_context_item - .turn_id - .clone() - .expect("thread settings should have turn_id"); - let compact_turn_id = "compact-turn".to_string(); - let rolled_back_turn_id = "rolled-back-turn".to_string(); - let compacted_history = vec![ - user_message("turn 1 user"), - user_message("summary after compaction"), - ]; - let first_window_id = Uuid::now_v7(); - let previous_window_id = Uuid::now_v7(); - let compacted_window_id = Uuid::now_v7(); - - sess.persist_rollout_items(&[ - RolloutItem::EventMsg(EventMsg::TurnStarted( - codex_protocol::protocol::TurnStartedEvent { - turn_id: first_turn_id.clone(), - root_turn_id: None, - trace_id: None, - started_at: None, - model_context_window: Some(128_000), - collaboration_mode_kind: ModeKind::Default, - }, - )), - RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { - client_id: None, - message: "turn 1 user".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - })), - RolloutItem::TurnContext(first_context_item.clone()), - RolloutItem::ResponseItem(user_message("turn 1 user").into()), - RolloutItem::ResponseItem(assistant_message("turn 1 assistant").into()), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: first_turn_id, - started_at: None, - last_agent_message: None, - error: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })), - RolloutItem::EventMsg(EventMsg::TurnStarted( - codex_protocol::protocol::TurnStartedEvent { - turn_id: compact_turn_id.clone(), - root_turn_id: None, - trace_id: None, - started_at: None, - model_context_window: Some(128_000), - collaboration_mode_kind: ModeKind::Default, - }, - )), - RolloutItem::Compacted(CompactedItem { - message: "summary after compaction".to_string(), - replacement_history: Some( - compacted_history - .iter() - .cloned() - .map(ResponseItemEnvelope::new) - .collect(), - ), - retained_context: None, - guardian_history: None, - mcp_resource_origins: None, - window_number: Some(7), - first_window_id: Some(first_window_id.to_string()), - previous_window_id: Some(previous_window_id.to_string()), - window_id: Some(compacted_window_id.to_string()), - compaction_response_id: None, - latest_token_usage_record: None, - }), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: compact_turn_id, - started_at: None, - last_agent_message: None, - error: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })), - RolloutItem::EventMsg(EventMsg::TurnStarted( - codex_protocol::protocol::TurnStartedEvent { - turn_id: rolled_back_turn_id.clone(), - root_turn_id: None, - trace_id: None, - started_at: None, - model_context_window: Some(128_000), - collaboration_mode_kind: ModeKind::Default, - }, - )), - RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { - client_id: None, - message: "turn 2 user".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - })), - RolloutItem::TurnContext(TurnContextItem { - turn_id: Some(rolled_back_turn_id.clone()), - model: "rolled-back-model".to_string(), - comp_hash: None, - ..first_context_item.clone() - }), - RolloutItem::ResponseItem(user_message("turn 2 user").into()), - RolloutItem::ResponseItem(assistant_message("turn 2 assistant").into()), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: rolled_back_turn_id, - started_at: None, - last_agent_message: None, - error: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })), - ]) - .await; - sess.replace_history( - vec![assistant_message("stale history")], - Some(first_context_item), - ) - .await; - { - let mut state = sess.state.lock().await; - state.restore_auto_compact_window( - /*window_number*/ 99, - AutoCompactWindowIds { - first_window_id: Uuid::now_v7(), - previous_window_id: Some(Uuid::now_v7()), - window_id: Uuid::now_v7(), - }, - ); - } - - handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; - let rollback_event = wait_for_thread_rolled_back(&rx).await; - assert_eq!(rollback_event.num_turns, 1); - - assert_eq!( - raw_history_items(&sess.clone_history().await), - compacted_history - ); - assert!(sess.reference_context_item().await.is_none()); - assert_eq!( - sess.state.lock().await.auto_compact_window_ids(), - AutoCompactWindowIds { - first_window_id, - previous_window_id: Some(previous_window_id), - window_id: compacted_window_id, - } - ); - assert!(sess.current_window_id().await.ends_with(":7")); -} - -#[tokio::test] -async fn thread_rollback_persists_marker_and_replays_cumulatively() { - let (mut sess, tc, rx) = make_session_and_context_with_rx().await; - let rollout_path = attach_thread_persistence( - Arc::get_mut(&mut sess).expect("session should not have additional references"), - ) - .await; - let turn_context_item = tc.to_turn_context_item(); - - sess.persist_rollout_items(&[ - RolloutItem::EventMsg(EventMsg::TurnStarted( - codex_protocol::protocol::TurnStartedEvent { - turn_id: "turn-1".to_string(), - root_turn_id: None, - trace_id: None, - started_at: None, - model_context_window: Some(128_000), - collaboration_mode_kind: ModeKind::Default, - }, - )), - RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { - client_id: None, - message: "turn 1 user".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - })), - RolloutItem::TurnContext(turn_context_item.clone()), - RolloutItem::ResponseItem(user_message("turn 1 user").into()), - RolloutItem::ResponseItem(assistant_message("turn 1 assistant").into()), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-1".to_string(), - started_at: None, - last_agent_message: None, - error: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })), - RolloutItem::EventMsg(EventMsg::TurnStarted( - codex_protocol::protocol::TurnStartedEvent { - turn_id: "turn-2".to_string(), - root_turn_id: None, - trace_id: None, - started_at: None, - model_context_window: Some(128_000), - collaboration_mode_kind: ModeKind::Default, - }, - )), - RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { - client_id: None, - message: "turn 2 user".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - })), - RolloutItem::TurnContext(turn_context_item.clone()), - RolloutItem::ResponseItem(user_message("turn 2 user").into()), - RolloutItem::ResponseItem(assistant_message("turn 2 assistant").into()), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-2".to_string(), - started_at: None, - last_agent_message: None, - error: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })), - RolloutItem::EventMsg(EventMsg::TurnStarted( - codex_protocol::protocol::TurnStartedEvent { - turn_id: "turn-3".to_string(), - root_turn_id: None, - trace_id: None, - started_at: None, - model_context_window: Some(128_000), - collaboration_mode_kind: ModeKind::Default, - }, - )), - RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { - client_id: None, - message: "turn 3 user".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - })), - RolloutItem::TurnContext(turn_context_item), - RolloutItem::ResponseItem(user_message("turn 3 user").into()), - RolloutItem::ResponseItem(assistant_message("turn 3 assistant").into()), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-3".to_string(), - started_at: None, - last_agent_message: None, - error: None, - completed_at: None, - duration_ms: None, - time_to_first_token_ms: None, - })), - ]) - .await; - - handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; - let first_rollback = wait_for_thread_rolled_back(&rx).await; - assert_eq!(first_rollback.num_turns, 1); - handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; - let second_rollback = wait_for_thread_rolled_back(&rx).await; - assert_eq!(second_rollback.num_turns, 1); - - assert_eq!( - raw_history_items(&sess.clone_history().await), - vec![ - user_message("turn 1 user"), - assistant_message("turn 1 assistant") - ] - ); - - let InitialHistory::Resumed(resumed) = RolloutRecorder::get_rollout_history(&rollout_path) - .await - .expect("read rollout history") - else { - panic!("expected resumed rollout history"); - }; - let rollback_markers = resumed - .history - .iter() - .filter(|item| matches!(item, RolloutItem::EventMsg(EventMsg::ThreadRolledBack(_)))) - .count(); - assert_eq!(rollback_markers, 2); -} - -#[tokio::test] -async fn thread_rollback_fails_when_turn_in_progress() { - let (sess, tc, rx) = make_session_and_context_with_rx().await; - - let initial_context = build_initial_context(&sess, &tc).await; - sess.record_conversation_items(tc.as_ref(), tc.model_info(), &initial_context) - .await; - let history_before_rollback = sess.clone_history().await; - - *sess.active_turn.lock().await = Some(crate::state::ActiveTurn::default()); - handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; - - let error_event = wait_for_thread_rollback_failed(&rx).await; - assert_eq!( - error_event.codex_error_info, - Some(CodexErrorInfo::ThreadRollbackFailed) - ); - - let history = sess.clone_history().await; - assert_eq!( - raw_history_items(&history_before_rollback), - raw_history_items(&history) - ); -} - -#[tokio::test] -async fn thread_rollback_fails_when_num_turns_is_zero() { - let (sess, tc, rx) = make_session_and_context_with_rx().await; - - let initial_context = build_initial_context(&sess, &tc).await; - sess.record_conversation_items(tc.as_ref(), tc.model_info(), &initial_context) - .await; - let history_before_rollback = sess.clone_history().await; - - handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 0).await; - - let error_event = wait_for_thread_rollback_failed(&rx).await; - assert_eq!(error_event.message, "num_turns must be >= 1"); - assert_eq!( - error_event.codex_error_info, - Some(CodexErrorInfo::ThreadRollbackFailed) - ); - - let history = sess.clone_history().await; - assert_eq!( - raw_history_items(&history_before_rollback), - raw_history_items(&history) - ); -} - #[tokio::test] async fn set_rate_limits_retains_previous_credits() { let codex_home = tempfile::tempdir().expect("create temp dir"); @@ -4965,42 +4361,6 @@ fn success_flag_true_with_no_error_and_content_used() { assert_eq!(expected, got); } -async fn wait_for_thread_rolled_back(rx: &async_channel::Receiver) -> ThreadRolledBackEvent { - let deadline = StdDuration::from_secs(2); - let start = std::time::Instant::now(); - loop { - let remaining = deadline.saturating_sub(start.elapsed()); - let evt = tokio::time::timeout(remaining, rx.recv()) - .await - .expect("timeout waiting for event") - .expect("event"); - match evt.msg { - EventMsg::ThreadRolledBack(payload) => return payload, - _ => continue, - } - } -} - -async fn wait_for_thread_rollback_failed(rx: &async_channel::Receiver) -> ErrorEvent { - let deadline = StdDuration::from_secs(2); - let start = std::time::Instant::now(); - loop { - let remaining = deadline.saturating_sub(start.elapsed()); - let evt = tokio::time::timeout(remaining, rx.recv()) - .await - .expect("timeout waiting for event") - .expect("event"); - match evt.msg { - EventMsg::Error(payload) - if payload.codex_error_info == Some(CodexErrorInfo::ThreadRollbackFailed) => - { - return payload; - } - _ => continue, - } - } -} - async fn open_thread_persistence(session: &mut Session) -> PathBuf { let config = session.get_config().await; let live_thread = LiveThread::create( diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 4fb1aac62f..777dc8963b 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -503,7 +503,7 @@ pub(crate) async fn run_turn( .record_step_world_state_if_changed(&world_state, step_context.as_ref()) .await?; - // Keep the override after accepted input so ordinary turn rollback removes it too. + // Keep the override after accepted input so history truncation removes them together. sess.record_reasoning_effort_override(step_context.as_ref()) .await; diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index d059d0502b..b45bec136a 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -31,8 +31,7 @@ use codex_utils_output_truncation::TruncationPolicy; use tokio_util::task::AbortOnDropHandle; /// Runtime request effort, initially unset and established by prewarm or sampling. -/// Rollback clears it after startup prewarm is consumed; successful compaction allows -/// a fresh baseline without an override. +/// Successful compaction allows a fresh baseline without an override. pub(crate) enum ReasoningEffortPin { Unset, Compacted, diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index b60a453d0a..c2372d4076 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -19,30 +19,21 @@ use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_extension_api::ExtensionRegistryBuilder; use codex_history::CodexHarnessMetadata; use codex_history::RolloutItem; -use codex_protocol::config_types::CollaborationMode; -use codex_protocol::config_types::ModeKind; -use codex_protocol::config_types::Settings; use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; -use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::WarningEvent; use codex_protocol::user_input::UserInput; use core_test_support::ThreadIdle; -use core_test_support::context_snapshot; -use core_test_support::context_snapshot::ContextSnapshotOptions; -use core_test_support::context_snapshot::ContextSnapshotRenderMode; use core_test_support::responses::ResponseMock; use core_test_support::responses::ResponsesRequest; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; -use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once_match; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; -use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; @@ -54,7 +45,6 @@ use tempfile::TempDir; use wiremock::MockServer; const AFTER_SECOND_RESUME: &str = "AFTER_SECOND_RESUME"; -const AFTER_ROLLBACK: &str = "AFTER_ROLLBACK"; const CHECKPOINT_METADATA_KEY: &str = "replacement_history_metadata"; fn network_disabled() -> bool { @@ -506,233 +496,6 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<() Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -/// Scenario: rolling back behind a pre-turn compaction should replay -/// append-only history from the rollout file and keep earlier compacted -/// history visible. -async fn snapshot_rollback_past_compaction_replays_append_only_history() -> Result<()> { - if network_disabled() { - println!("Skipping test because network is disabled in this sandbox"); - return Ok(()); - } - - const EDITED_AFTER_COMPACT: &str = "EDITED_AFTER_COMPACT"; - const SECOND_REPLY: &str = "SECOND_REPLY"; - - let server = MockServer::start().await; - let sse1 = sse(vec![ - ev_assistant_message("m1", FIRST_REPLY), - ev_completed("r1"), - ]); - let sse2 = sse(vec![ - ev_assistant_message("m2", SUMMARY_TEXT), - ev_completed("r2"), - ]); - let sse3 = sse(vec![ - ev_assistant_message("m3", SECOND_REPLY), - ev_completed("r3"), - ]); - let sse4 = sse(vec![ev_completed("r4")]); - - let request_log = mount_sse_sequence(&server, vec![sse1, sse2, sse3, sse4]).await; - - let (_home, _config, _manager, base) = start_test_conversation(&server, /*model*/ None).await; - - user_turn(&base, "hello world").await; - compact_conversation(&base).await; - user_turn(&base, EDITED_AFTER_COMPACT).await; - - base.submit(Op::ThreadRollback { num_turns: 1 }) - .await - .expect("submit thread rollback"); - let rollback_event = wait_for_event(&base, |ev| { - if let EventMsg::Error(error) = ev { - panic!("rollback failed: {error:?}"); - } - matches!(ev, EventMsg::ThreadRolledBack(_)) - }) - .await; - let EventMsg::ThreadRolledBack(rollback_event) = rollback_event else { - panic!("expected thread rolled back event"); - }; - assert_eq!(rollback_event.num_turns, 1); - - user_turn(&base, AFTER_ROLLBACK).await; - - let requests = request_log.requests(); - assert_eq!(requests.len(), 4); - assert!(requests[1].body_contains_text(SUMMARIZATION_PROMPT)); - assert!(requests[2].body_contains_text("hello world")); - assert!(requests[2].body_contains_text(SUMMARY_TEXT)); - assert!(requests[2].body_contains_text(EDITED_AFTER_COMPACT)); - let after_rollback_user_texts = requests[3].message_input_texts("user"); - let after_rollback_last = after_rollback_user_texts - .last() - .expect("post-rollback request missing user messages"); - assert_eq!(after_rollback_last, AFTER_ROLLBACK); - assert!( - requests[3].body_contains_text("hello world"), - "the first turn should remain visible after rollback behind compaction", - ); - assert!( - !requests[3].body_contains_text(EDITED_AFTER_COMPACT), - "the edited post-compaction turn should be removed by rollback", - ); - assert!( - requests[3].body_contains_text(SUMMARY_TEXT), - "compaction summary should remain for the preserved first turn", - ); - - insta::assert_snapshot!( - "rollback_past_compaction_shapes", - context_snapshot::format_labeled_requests_snapshot( - "rollback past compaction replay after rollback", - &[ - ("compaction request", &requests[1]), - ("before rollback", &requests[2]), - ("after rollback", &requests[3]), - ], - &ContextSnapshotOptions::default() - .strip_capability_instructions() - .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }), - ) - ); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -/// Scenario: rolling back a turn that introduced persistent pre-thread settings -/// diffs should trim those context updates so the next request includes them -/// only once. -async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> { - if network_disabled() { - println!("Skipping test because network is disabled in this sandbox"); - return Ok(()); - } - - const MODEL: &str = "gpt-5.4"; - const TURN_ONE_USER: &str = "turn 1 user"; - const TURN_TWO_USER: &str = "turn 2 user"; - const FOLLOWUP_USER: &str = "follow-up user"; - const ROLLED_BACK_DEV_INSTRUCTIONS: &str = "ROLLED_BACK_DEV_INSTRUCTIONS"; - const PRETURN_CONTEXT_DIFF_CWD: &str = "PRETURN_CONTEXT_DIFF_CWD"; - - let server = MockServer::start().await; - let request_log = mount_sse_sequence( - &server, - vec![ - sse(vec![ - ev_assistant_message("m1", "turn 1 assistant"), - ev_completed("r1"), - ]), - sse(vec![ - ev_assistant_message("m2", "turn 2 assistant"), - ev_completed("r2"), - ]), - sse(vec![ev_response_created("r3"), ev_completed("r3")]), - ], - ) - .await; - - let (_home, config, _manager, conversation) = - start_test_conversation(&server, Some(MODEL)).await; - - user_turn(&conversation, TURN_ONE_USER).await; - - let override_cwd = config.cwd.join(PRETURN_CONTEXT_DIFF_CWD); - std::fs::create_dir_all(&override_cwd)?; - core_test_support::submit_thread_settings( - &conversation, - ThreadSettingsOverrides { - environments: Some(local_selections(override_cwd.clone())), - collaboration_mode: Some(CollaborationMode { - mode: ModeKind::Default, - settings: Settings { - model: MODEL.to_string(), - reasoning_effort: None, - developer_instructions: Some(ROLLED_BACK_DEV_INSTRUCTIONS.to_string()), - }, - }), - ..Default::default() - }, - ) - .await?; - - user_turn(&conversation, TURN_TWO_USER).await; - - conversation - .submit(Op::ThreadRollback { num_turns: 1 }) - .await?; - let rollback_event = wait_for_event(&conversation, |ev| { - if let EventMsg::Error(error) = ev { - panic!("rollback failed: {error:?}"); - } - matches!(ev, EventMsg::ThreadRolledBack(_)) - }) - .await; - let EventMsg::ThreadRolledBack(rollback_event) = rollback_event else { - panic!("expected thread rolled back event"); - }; - assert_eq!(rollback_event.num_turns, 1); - - user_turn(&conversation, FOLLOWUP_USER).await; - - let requests = request_log.requests(); - assert_eq!(requests.len(), 3); - - let before_rollback_developer_count = requests[1] - .message_input_texts("developer") - .iter() - .filter(|text| text.contains(ROLLED_BACK_DEV_INSTRUCTIONS)) - .count(); - assert_eq!(before_rollback_developer_count, 1); - assert_eq!( - requests[1] - .message_input_texts("user") - .iter() - .filter(|text| text.contains(PRETURN_CONTEXT_DIFF_CWD)) - .count(), - 1 - ); - - let after_rollback_developer_count = requests[2] - .message_input_texts("developer") - .iter() - .filter(|text| text.contains(ROLLED_BACK_DEV_INSTRUCTIONS)) - .count(); - assert_eq!(after_rollback_developer_count, 1); - - let after_rollback_user_texts = requests[2].message_input_texts("user"); - assert_eq!( - after_rollback_user_texts - .iter() - .filter(|text| text.contains(PRETURN_CONTEXT_DIFF_CWD)) - .count(), - 1 - ); - assert_eq!( - after_rollback_user_texts.last().map(String::as_str), - Some(FOLLOWUP_USER) - ); - - insta::assert_snapshot!( - "rollback_followup_turn_trims_context_updates", - context_snapshot::format_labeled_requests_snapshot( - "rollback trims pre-turn override context updates before the follow-up request", - &[ - ("rolled-back turn request", &requests[1]), - ("follow-up request after rollback", &requests[2]), - ], - &ContextSnapshotOptions::default() - .strip_capability_instructions() - .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 96 }), - ) - ); - - Ok(()) -} - fn normalize_line_endings(value: &mut Value) { match value { Value::String(text) if text.contains('\r') => { diff --git a/codex-rs/core/tests/suite/guardian_authorization.rs b/codex-rs/core/tests/suite/guardian_authorization.rs index e793c5cb21..91e3ade7c7 100644 --- a/codex-rs/core/tests/suite/guardian_authorization.rs +++ b/codex-rs/core/tests/suite/guardian_authorization.rs @@ -217,8 +217,7 @@ async fn guardian_revalidates_owning_session_before_allow( } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn guardian_authorization_revision_survives_compaction_not_user_input_or_rollback() --> Result<()> { +async fn guardian_authorization_revision_survives_compaction_not_user_input() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -285,14 +284,5 @@ async fn guardian_authorization_revision_survives_compaction_not_user_input_or_r .await; assert_eq!(test.codex.guardian_authorization_version().await, expected); - test.codex.ensure_rollout_materialized().await; - test.codex - .submit(Op::ThreadRollback { num_turns: 1 }) - .await?; - wait_for_event(&test.codex, |event| { - matches!(event, EventMsg::ThreadRolledBack(_)) - }) - .await; - assert_ne!(test.codex.guardian_authorization_version().await, expected); Ok(()) } diff --git a/codex-rs/core/tests/suite/guardian_history.rs b/codex-rs/core/tests/suite/guardian_history.rs index 1438fd937e..5ccc0fa328 100644 --- a/codex-rs/core/tests/suite/guardian_history.rs +++ b/codex-rs/core/tests/suite/guardian_history.rs @@ -1,4 +1,4 @@ -//! Exercises retained review history through compaction, resume, fork, eviction, and rollback. +//! Exercises retained review history through compaction, resume, fork, eviction, and legacy rollback replay. use anyhow::Result; use base64::Engine; @@ -17,6 +17,7 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadHistoryMode; +use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_protocol::user_input::UserInput; @@ -310,14 +311,15 @@ async fn guardian_history_uses_deltas_between_eviction_batches() -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn guardian_history_survives_compaction_and_eviction_but_not_rollback() -> Result<()> { +async fn guardian_history_survives_compaction_and_eviction_but_not_legacy_rollback_replay() +-> Result<()> { skip_if_no_network!(Ok(())); skip_if_wine_exec!( Ok(()), "Guardian approval actions require host-native paths" ); let server = start_mock_server().await; - let test = test_codex() + let mut test = test_codex() .with_config(|config| { config.features.enable(Feature::TokenBudget).unwrap(); config @@ -499,12 +501,35 @@ async fn guardian_history_survives_compaction_and_eviction_but_not_rollback() -> ); test.codex.ensure_rollout_materialized().await; test.codex - .submit(Op::ThreadRollback { num_turns: 2 }) + .append_rollout_items(&[RolloutItem::EventMsg(EventMsg::ThreadRolledBack( + ThreadRolledBackEvent { num_turns: 2 }, + ))]) .await?; - wait_for_event(&test.codex, |event| { - matches!(event, EventMsg::ThreadRolledBack(_)) - }) - .await; + test.codex.shutdown_and_wait().await?; + let thread_id = test.session_configured.thread_id; + test.thread_manager.remove_thread(&thread_id).await; + let model_context = test + .thread_store + .load_latest_model_context(LoadThreadHistoryParams { + thread_id, + include_archived: false, + }) + .await?; + test.codex = test + .thread_manager + .resume_thread_with_history( + test.config.clone(), + InitialHistory::Resumed(ResumedHistory { + conversation_id: thread_id, + history: Arc::new(model_context.items), + rollout_path: None, + }), + test.thread_manager.auth_manager(), + /*parent_trace*/ None, + ClientMcpExtensions::default(), + ) + .await? + .thread; } else { assert!(!transcript.contains(">>> TRUSTED USER ANSWERS START")); assert!(!transcript.contains("Do not publish anything.")); diff --git a/codex-rs/core/tests/suite/guardian_retained_context.rs b/codex-rs/core/tests/suite/guardian_retained_context.rs index 30d63fe815..d1c073b971 100644 --- a/codex-rs/core/tests/suite/guardian_retained_context.rs +++ b/codex-rs/core/tests/suite/guardian_retained_context.rs @@ -1,5 +1,5 @@ //! Retained-instruction and answer lifecycles through real sessions and durable checkpoints. -//! Real user input covers steering, compaction and rollback; legacy replay fixtures also use +//! Real user input covers steering and compaction; legacy rollback replay fixtures use //! the public rollout append API. Resume and child forks use production paths. use std::collections::HashMap; @@ -36,6 +36,7 @@ use codex_protocol::models::ResponseItem; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadHistoryMode; +use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_protocol::user_input::UserInput; @@ -63,7 +64,7 @@ use wiremock::MockServer; use wiremock::matchers::header; // Keep child completion out of the parent's history and wait for parent turn cleanup -// before checking rollback boundaries. +// before checking inherited authorization. #[derive(Default)] struct ForkTestLifecycle { entered: Notify, @@ -104,7 +105,7 @@ async fn wait_for_thread_idle(thread: &CodexThread) { .get_or_init(ThreadIdle::default); tokio::time::timeout(Duration::from_secs(10), idle.0.notified()) .await - .expect("thread should become idle before rollback"); + .expect("thread should become idle"); } async fn record_answer( @@ -220,7 +221,7 @@ async fn compact_and_assert_answers( expected: &[VerifiedAnswer], ) -> Result { // Inspect live state by persisting a real compaction checkpoint, not a private getter. - // Repeating this after resume/rollback also catches checkpoint resurrection. + // Repeating this after legacy rollback replay also catches checkpoint resurrection. thread.submit(Op::Compact).await?; wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; thread.flush_rollout().await?; @@ -250,11 +251,11 @@ enum InstructionSize { Oversized, } -#[test_case(ThreadHistoryMode::Legacy, true, InstructionSize::Normal; "enabled legacy rollback")] +#[test_case(ThreadHistoryMode::Legacy, true, InstructionSize::Normal; "enabled legacy rollback replay")] #[test_case(ThreadHistoryMode::Paginated, true, InstructionSize::Normal; "enabled paginated resume")] -#[test_case(ThreadHistoryMode::Legacy, false, InstructionSize::Normal; "disabled legacy rollback")] +#[test_case(ThreadHistoryMode::Legacy, false, InstructionSize::Normal; "disabled legacy rollback replay")] #[test_case(ThreadHistoryMode::Paginated, false, InstructionSize::Normal; "disabled paginated resume")] -#[test_case(ThreadHistoryMode::Legacy, true, InstructionSize::Oversized; "oversized instruction rollback")] +#[test_case(ThreadHistoryMode::Legacy, true, InstructionSize::Oversized; "oversized instruction rollback replay")] #[test_case(ThreadHistoryMode::Paginated, true, InstructionSize::Oversized; "oversized instruction resume")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn retained_instructions_keep_identity_across_compaction_and_resume( @@ -272,8 +273,7 @@ async fn retained_instructions_keep_identity_across_compaction_and_resume( "Project detail. ".repeat(2_000) ), }; - // Paginated history supports checkpoint resume, but not the full-history read - // used by ThreadRollback. Exercise rollback on its supported legacy path. + // Legacy rollouts can contain rollback markers; paginated histories use checkpoint resume. let rollback_counts: &[usize] = match history_mode { ThreadHistoryMode::Legacy => &[1, 0], ThreadHistoryMode::Paginated => &[], @@ -496,11 +496,12 @@ async fn retained_instructions_keep_identity_across_compaction_and_resume( serde_json::Value::Null } ); - thread.submit(Op::ThreadRollback { num_turns: 1 }).await?; - wait_for_event(&thread, |event| { - matches!(event, EventMsg::ThreadRolledBack(_)) - }) - .await; + thread + .append_rollout_items(&[RolloutItem::EventMsg(EventMsg::ThreadRolledBack( + ThreadRolledBackEvent { num_turns: 1 }, + ))]) + .await?; + thread = resume(&test, &thread).await?; expected["user_messages"] .as_array_mut() .expect("expected retained user messages") @@ -772,7 +773,7 @@ async fn disabled_capture_stays_incomplete_after_compaction_and_enabled_resume() } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn retained_answers_rollback_only_the_steered_instruction() -> Result<()> { +async fn legacy_rollback_replay_retains_only_surviving_steered_answers() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let test = test_codex() @@ -907,11 +908,12 @@ async fn retained_answers_rollback_only_the_steered_instruction() -> Result<()> let mut thread = resume(&test, &test.codex).await?; compact_and_assert_answers(&test, &thread, &answers).await?; for expected in [&answers[..1], &[]] { - thread.submit(Op::ThreadRollback { num_turns: 1 }).await?; - wait_for_event(&thread, |event| { - matches!(event, EventMsg::ThreadRolledBack(_)) - }) - .await; + thread + .append_rollout_items(&[RolloutItem::EventMsg(EventMsg::ThreadRolledBack( + ThreadRolledBackEvent { num_turns: 1 }, + ))]) + .await?; + thread = resume(&test, &thread).await?; compact_and_assert_answers(&test, &thread, expected).await?; thread = resume(&test, &thread).await?; compact_and_assert_answers(&test, &thread, expected).await?; @@ -922,7 +924,7 @@ async fn retained_answers_rollback_only_the_steered_instruction() -> Result<()> #[derive(Clone, Copy)] enum LifecycleBoundary { - Rollback, + LegacyRollbackReplay, ChildFork, } @@ -1221,8 +1223,8 @@ async fn forked_parent_instructions_do_not_become_local_authorization( sse(vec![ev_completed("parent-fork-completion")]), ) .await; - // Hold the child at a real tool boundary until rollback is checked. Its completion - // notification must not add a parent message or consume a parent mock response. + // Pause the child while checking inherited authorization. Its completion notification + // must not add a parent message or consume a parent mock response. let child_requests = mount_sse_once_match( &server, move |request: &wiremock::Request| { @@ -1274,36 +1276,9 @@ async fn forked_parent_instructions_do_not_become_local_authorization( thread_context_enabled.then_some((0, true)), "inherited conversation must not populate child-local authorization", ); - let root_before = child.guardian_root_snapshot().await.context("live root")?; + let root_snapshot = child.guardian_root_snapshot().await.context("live root")?; assert!( - root_before - .messages - .contains(&codex_core::GuardianRootMessage::User( - PARENT_GRANT.to_owned() - )) - ); - - wait_for_thread_idle(&test.codex).await; - test.codex - .submit(Op::ThreadRollback { num_turns: 1 }) - .await?; - wait_for_event(&test.codex, |event| { - if let EventMsg::Error(error) = event { - panic!("rollback failed: {error:?}"); - } - matches!(event, EventMsg::ThreadRolledBack(_)) - }) - .await; - let root_after = child - .guardian_root_snapshot() - .await - .context("live root after rollback")?; - assert_ne!( - root_before.authorization_version, - root_after.authorization_version - ); - assert!( - !root_after + root_snapshot .messages .contains(&codex_core::GuardianRootMessage::User( PARENT_GRANT.to_owned() @@ -1368,7 +1343,7 @@ async fn forked_parent_instructions_do_not_become_local_authorization( Ok(()) } -#[test_case(ThreadHistoryMode::Legacy, LifecycleBoundary::Rollback; "legacy rollback")] +#[test_case(ThreadHistoryMode::Legacy, LifecycleBoundary::LegacyRollbackReplay; "legacy rollback replay")] #[test_case(ThreadHistoryMode::Legacy, LifecycleBoundary::ChildFork; "legacy child fork")] #[test_case(ThreadHistoryMode::Paginated, LifecycleBoundary::ChildFork; "paginated child fork")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1455,15 +1430,16 @@ async fn retained_answers_cross_real_session_boundaries( let expected = [before.clone(), after]; match boundary { - LifecycleBoundary::Rollback => { + LifecycleBoundary::LegacyRollbackReplay => { // First remove the suffix answer, then the source retained only in the checkpoint. let mut thread = thread; for expected in [std::slice::from_ref(&before), &[]] { - thread.submit(Op::ThreadRollback { num_turns: 1 }).await?; - wait_for_event(&thread, |event| { - matches!(event, EventMsg::ThreadRolledBack(_)) - }) - .await; + thread + .append_rollout_items(&[RolloutItem::EventMsg(EventMsg::ThreadRolledBack( + ThreadRolledBackEvent { num_turns: 1 }, + ))]) + .await?; + thread = resume(&test, &thread).await?; compact_and_assert_answers(&test, &thread, expected).await?; thread = resume(&test, &thread).await?; compact_and_assert_answers(&test, &thread, expected).await?; diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 7338b7b437..6e0dc6df59 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -3,12 +3,7 @@ use codex_config::types::Personality; use codex_core::CodexThread; use codex_core::ForkSnapshot; use codex_core::TurnInputRequest; -use codex_core::config::Config; use codex_core::config::Constrained; -use codex_extension_api::ExtensionFuture; -use codex_extension_api::ExtensionRegistryBuilder; -use codex_extension_api::ThreadIdleInput; -use codex_extension_api::ThreadLifecycleContributor; use codex_features::Feature; use codex_history::RolloutItem; use codex_login::CodexAuth; @@ -64,9 +59,7 @@ use pretty_assertions::assert_eq; use serde_json::json; use std::collections::HashMap; use std::sync::Arc; -use std::time::Duration; use test_case::test_case; -use tokio::sync::Notify; use wiremock::MockServer; fn read_only_user_turn(test: &TestCodex, items: Vec, model: String) -> TurnInputRequest { @@ -303,131 +296,6 @@ async fn first_turn_after_empty_prefix_fork_preserves_inherited_base_instruction Ok(()) } -#[derive(Clone, Copy)] -enum RollbackFollowup { - StartupModel, - SwitchedModel, - ColdResume, -} - -#[derive(Default)] -struct RollbackReady { - idle: Notify, -} - -impl ThreadLifecycleContributor for RollbackReady { - fn on_thread_idle<'a>(&'a self, _input: ThreadIdleInput<'a>) -> ExtensionFuture<'a, ()> { - Box::pin(async move { - self.idle.notify_one(); - }) - } -} - -impl RollbackReady { - async fn wait(&self) { - // TurnComplete is delivered before the active turn is cleared. Rollback requires - // the later thread-idle callback so it cannot race with turn cleanup. - tokio::time::timeout(Duration::from_secs(10), self.idle.notified()) - .await - .expect("thread should become idle before rollback"); - } -} - -#[test_case(RollbackFollowup::StartupModel; "return to startup model")] -#[test_case(RollbackFollowup::SwitchedModel; "retry switched model")] -#[test_case(RollbackFollowup::ColdResume; "retry switched model after cold resume")] -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn rollback_first_turn_model_change_removes_its_instructions( - followup: RollbackFollowup, -) -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = MockServer::start().await; - let response_mock = mount_sse_sequence( - &server, - vec![sse_completed("resp-first"), sse_completed("resp-followup")], - ) - .await; - - let initial_model = "gpt-5.6-terra"; - let switched_model = "gpt-5.5"; - let rollback_ready = Arc::new(RollbackReady::default()); - let mut extensions = ExtensionRegistryBuilder::new(); - extensions.thread_lifecycle_contributor(rollback_ready.clone()); - let mut builder = test_codex() - .with_model_info_override(initial_model, configure_model_switching_fixture) - .with_extensions(Arc::new(extensions.build())); - let test = builder.build_with_auto_env(&server).await?; - - submit_model_turn( - &test.codex, - switched_model, - ThreadSettingsOverrides::default(), - ) - .await?; - - rollback_ready.wait().await; - test.codex - .submit(Op::ThreadRollback { num_turns: 1 }) - .await?; - wait_for_event(&test.codex, |ev| { - if let EventMsg::Error(error) = ev { - panic!("rollback failed: {error:?}"); - } - matches!(ev, EventMsg::ThreadRolledBack(_)) - }) - .await; - - let test = match followup { - RollbackFollowup::ColdResume => { - let mut resume_builder = test_codex() - .with_model_info_override(initial_model, configure_model_switching_fixture) - .with_model(switched_model); - resume_builder.restart(&server, &test).await? - } - RollbackFollowup::StartupModel | RollbackFollowup::SwitchedModel => test, - }; - let followup_model = match followup { - RollbackFollowup::StartupModel => initial_model, - RollbackFollowup::SwitchedModel | RollbackFollowup::ColdResume => switched_model, - }; - submit_model_turn( - &test.codex, - followup_model, - ThreadSettingsOverrides::default(), - ) - .await?; - - let request = &response_mock.requests()[1]; - assert_eq!(request.body_json()["model"], followup_model); - let misaligned_messages = request - .inputs_of_type("message") - .into_iter() - .filter(|message| { - message["internal_chat_message_metadata_passthrough"]["content_item_kinds"] - .as_array() - .is_some_and(|kinds| { - message["content"] - .as_array() - .is_none_or(|content| content.len() != kinds.len()) - }) - }) - .collect::>(); - assert_eq!(misaligned_messages, Vec::::new()); - let model_switch_count = request - .message_input_texts("developer") - .iter() - .filter(|text| text.contains("")) - .count(); - assert_eq!( - model_switch_count, - usize::from(followup_model == switched_model), - "rolled-back model instructions must not survive or be duplicated" - ); - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn model_change_appends_model_instructions_developer_message() -> Result<()> { skip_if_no_network!(Ok(())); @@ -1359,113 +1227,6 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = MockServer::start().await; - let image_model_slug = "test-image-model"; - let image_model = test_model_info( - image_model_slug, - "Test Image Model", - "supports image input", - default_input_modalities(), - ); - mount_models_once( - &server, - ModelsResponse { - models: vec![image_model], - }, - ) - .await; - - let responses = mount_sse_sequence( - &server, - vec![ - sse(vec![ - ev_response_created("resp-1"), - ev_image_generation_call("ig_rollback", "completed", "lobster", "Zm9v"), - ev_completed_with_tokens("resp-1", /*total_tokens*/ 10), - ]), - sse_completed("resp-2"), - ], - ) - .await; - - let rollback_ready = Arc::new(RollbackReady::default()); - let mut extensions = ExtensionRegistryBuilder::new(); - extensions.thread_lifecycle_contributor(rollback_ready.clone()); - let mut builder = test_codex() - .with_extensions(Arc::new(extensions.build())) - .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_config(move |config| { - config.model = Some(image_model_slug.to_string()); - }); - let test = builder.build(&server).await?; - let models_manager = test.thread_manager.get_models_manager(); - let _ = models_manager - .list_models( - RefreshStrategy::OnlineIfUncached, - codex_core::test_support::default_http_client_factory(), - ) - .await; - - test.codex - .start_or_steer_turn(read_only_user_turn( - &test, - vec![UserInput::Text { - text: "generate a lobster".to_string(), - text_elements: Vec::new(), - }], - image_model_slug.to_string(), - )) - .await?; - wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - - rollback_ready.wait().await; - test.codex - .submit(Op::ThreadRollback { num_turns: 1 }) - .await?; - wait_for_event(&test.codex, |ev| { - if let EventMsg::Error(error) = ev { - panic!("rollback failed: {error:?}"); - } - matches!(ev, EventMsg::ThreadRolledBack(_)) - }) - .await; - - test.codex - .start_or_steer_turn(read_only_user_turn( - &test, - vec![UserInput::Text { - text: "after rollback".to_string(), - text_elements: Vec::new(), - }], - image_model_slug.to_string(), - )) - .await?; - wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - - let requests = responses.requests(); - assert_eq!(requests.len(), 2, "expected two model requests"); - - let second_request = requests.last().expect("expected second request"); - assert!( - !second_request - .message_input_texts("user") - .iter() - .any(|text| text == "generate a lobster"), - "rollback should remove the rolled-back image-generation user turn" - ); - assert!( - second_request - .inputs_of_type("image_generation_call") - .is_empty(), - "rollback should remove the generated image call with the rolled-back turn" - ); - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/core/tests/suite/reasoning_effort_override.rs b/codex-rs/core/tests/suite/reasoning_effort_override.rs index e6420e0a14..ab54d07039 100644 --- a/codex-rs/core/tests/suite/reasoning_effort_override.rs +++ b/codex-rs/core/tests/suite/reasoning_effort_override.rs @@ -7,10 +7,6 @@ use codex_core::SuspendTurnOutcome; use codex_core::TurnInputRequest; use codex_core::TurnInputSubmission; use codex_core::config::Config; -use codex_extension_api::ExtensionFuture; -use codex_extension_api::ExtensionRegistryBuilder; -use codex_extension_api::ThreadIdleInput; -use codex_extension_api::ThreadLifecycleContributor; use codex_features::Feature; use codex_login::CodexAuth; use codex_protocol::models::ResponseItem; @@ -32,8 +28,6 @@ use serde_json::Value; use std::sync::Arc; use std::time::Duration; use test_case::test_case; -use tokio::sync::Notify; -use tokio::time::timeout; use wiremock::ResponseTemplate; fn override_builder() -> TestCodexBuilder { @@ -378,8 +372,6 @@ enum PrewarmStartup { New, Resume, Fork, - ResumeThenRollback, - ForkThenRollback, } #[test_case(true, PrewarmStartup::New; "new thread feature enabled")] @@ -388,8 +380,6 @@ enum PrewarmStartup { #[test_case(false, PrewarmStartup::Resume; "resumed thread feature disabled")] #[test_case(true, PrewarmStartup::Fork; "forked thread feature enabled")] #[test_case(false, PrewarmStartup::Fork; "forked thread feature disabled")] -#[test_case(true, PrewarmStartup::ResumeThenRollback; "resumed thread rollback before first turn")] -#[test_case(true, PrewarmStartup::ForkThenRollback; "forked thread rollback before first turn")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn reasoning_effort_override_websocket_prewarm_preserves_baseline( feature_enabled: bool, @@ -443,10 +433,7 @@ async fn reasoning_effort_override_websocket_prewarm_preserves_baseline( .map(String::as_str), Some("previous turn"), ); - if matches!( - startup, - PrewarmStartup::Fork | PrewarmStartup::ForkThenRollback - ) { + if matches!(startup, PrewarmStartup::Fork) { previous.codex.shutdown_and_wait().await?; let mut config = previous.config.clone(); configure_prewarm(&mut config); @@ -475,23 +462,6 @@ async fn reasoning_effort_override_websocket_prewarm_preserves_baseline( .await?; assert_eq!(warmup.body_json()["generate"], false); assert_eq!(warmup.body_json()["reasoning"]["effort"], "medium"); - if matches!( - startup, - PrewarmStartup::ResumeThenRollback | PrewarmStartup::ForkThenRollback - ) { - // Observing the warmup request guarantees that Medium is pinned before rollback. - test.codex - .submit(Op::ThreadRollback { num_turns: 1 }) - .await?; - let rollback = wait_for_event(&test.codex, |event| { - matches!(event, EventMsg::ThreadRolledBack(_) | EventMsg::Error(_)) - }) - .await; - assert!( - matches!(rollback, EventMsg::ThreadRolledBack(_)), - "rollback failed: {rollback:?}", - ); - } submit_thread_settings( &test.codex, ThreadSettingsOverrides { @@ -765,124 +735,6 @@ async fn reasoning_effort_override_disabled_on_resume_retires_update_at_compacti Ok(()) } -#[test_case(1, ReasoningEffort::High; "later turn keep changed selection")] -#[test_case(1, ReasoningEffort::Medium; "later turn match restored settings")] -#[test_case(2, ReasoningEffort::High; "through first turn")] -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn reasoning_effort_override_rollback_reestablishes_selected_effort( - num_turns: u32, - effort: ReasoningEffort, -) -> anyhow::Result<()> { - skip_if_no_network!(Ok(())); - #[derive(Default)] - struct ThreadIdle { - ready: Notify, - } - - impl ThreadLifecycleContributor for ThreadIdle { - fn on_thread_idle<'a>(&'a self, _input: ThreadIdleInput<'a>) -> ExtensionFuture<'a, ()> { - Box::pin(async move { - self.ready.notify_one(); - }) - } - } - - let idle = Arc::new(ThreadIdle::default()); - let mut extensions = ExtensionRegistryBuilder::new(); - extensions.thread_lifecycle_contributor(idle.clone()); - let server = responses::start_mock_server().await; - let first = responses::mount_sse_once( - &server, - responses::sse(vec![responses::ev_completed("first")]), - ) - .await; - let removed = responses::mount_sse_once( - &server, - responses::sse(vec![responses::ev_completed("removed")]), - ) - .await; - let replacement = responses::mount_sse_once( - &server, - responses::sse(vec![responses::ev_completed("replacement")]), - ) - .await; - let test = override_builder() - .with_extensions(Arc::new(extensions.build())) - .build_with_auto_env(&server) - .await?; - test.submit_text_turn("first turn").await?; - timeout(Duration::from_secs(/*secs*/ 10), idle.ready.notified()).await?; - submit_thread_settings( - &test.codex, - ThreadSettingsOverrides { - effort: Some(Some(ReasoningEffort::High)), - ..Default::default() - }, - ) - .await?; - test.submit_text_turn("removed turn").await?; - // Completion can be delivered before the active slot is cleared. Wait for - // actual idle so this test exercises effort replay, not rollback admission. - timeout(Duration::from_secs(/*secs*/ 10), idle.ready.notified()).await?; - test.codex.submit(Op::ThreadRollback { num_turns }).await?; - let rollback = wait_for_event(&test.codex, |event| { - matches!(event, EventMsg::ThreadRolledBack(_) | EventMsg::Error(_)) - }) - .await; - let EventMsg::ThreadRolledBack(rollback) = rollback else { - panic!("rollback failed: {rollback:?}"); - }; - assert_eq!(rollback.num_turns, num_turns); - submit_thread_settings( - &test.codex, - ThreadSettingsOverrides { - effort: Some(Some(effort.clone())), - ..Default::default() - }, - ) - .await?; - test.submit_text_turn("replacement turn").await?; - timeout(Duration::from_secs(/*secs*/ 10), idle.ready.notified()).await?; - - assert_eq!( - effort_updates(&first.single_request()), - [effort_update(ReasoningEffort::Medium)] - ); - assert_eq!( - effort_updates(&removed.single_request()), - [ - effort_update(ReasoningEffort::Medium), - effort_update(ReasoningEffort::High) - ] - ); - let request = replacement.single_request(); - assert_eq!(request.body_json()["reasoning"]["effort"], effort.as_str()); - let mut expected = if num_turns == 1 { - vec![effort_update(ReasoningEffort::Medium)] - } else { - Vec::new() - }; - expected.push(effort_update(effort.clone())); - assert_eq!(effort_updates(&request), expected); - let input = request.input(); - assert!( - !input - .iter() - .any(|item| item["content"][0]["text"] == "removed turn") - ); - let tail = responses::strip_response_item_ids_from_json(responses::strip_metadata_from_json( - Value::Array(input[input.len() - 2..].to_vec()), - )); - assert_eq!( - tail, - serde_json::json!([ - message("user", "replacement turn"), - effort_update(effort.clone()), - ]) - ); - Ok(()) -} - #[test_case(ReasoningEffort::Medium; "same selection")] #[test_case(ReasoningEffort::High; "changed selection")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/codex-rs/core/tests/suite/rollout_budget.rs b/codex-rs/core/tests/suite/rollout_budget.rs index 99761755e3..8192a3f0c5 100644 --- a/codex-rs/core/tests/suite/rollout_budget.rs +++ b/codex-rs/core/tests/suite/rollout_budget.rs @@ -1,14 +1,12 @@ use anyhow::Result; use codex_core::TurnInputRequest; use codex_core::config::RolloutBudgetConfig; -use codex_extension_api::ExtensionRegistryBuilder; use codex_features::Feature; use codex_model_provider_info::built_in_model_providers; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::user_input::UserInput; -use core_test_support::ThreadIdle; use core_test_support::responses::ResponsesRequest; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -24,7 +22,6 @@ use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde_json::json; -use std::sync::Arc; use std::time::Duration; use test_case::test_case; use tokio::time::timeout; @@ -468,56 +465,3 @@ async fn restates_the_current_remainder_after_compaction() -> Result<()> { Ok(()) } - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn restates_the_current_remainder_after_rollback() -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = start_mock_server().await; - let responses = mount_sse_sequence( - &server, - vec![ - sse(vec![ - ev_response_created("resp-1"), - ev_completed_with_tokens("resp-1", /*total_tokens*/ 30), - ]), - sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), - ], - ) - .await; - let mut extensions = ExtensionRegistryBuilder::new(); - extensions.thread_lifecycle_contributor(Arc::new(ThreadIdle)); - let test = test_codex() - .with_extensions(Arc::new(extensions.build())) - .with_config(|config| { - config.rollout_budget = Some(RolloutBudgetConfig { - reminder_at_remaining_tokens: vec![50], - ..rollout_budget() - }); - }) - .build(&server) - .await?; - - test.submit_turn("rolled-back turn").await?; - ThreadIdle::wait(&test.codex).await; - test.codex - .submit(Op::ThreadRollback { num_turns: 1 }) - .await?; - wait_for_event(&test.codex, |event| { - if let EventMsg::Error(error) = event { - panic!("rollback failed: {error:?}"); - } - matches!(event, EventMsg::ThreadRolledBack(_)) - }) - .await; - test.submit_turn("turn after rollback").await?; - - let requests = responses.requests(); - assert_eq!( - rollout_budget_texts(&requests[1]), - vec![rollout_budget_message(/*remaining_tokens*/ 70)], - "rollback should rearm the current budget reminder without refunding usage" - ); - - Ok(()) -} diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_followup_turn_trims_context_updates.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_followup_turn_trims_context_updates.snap deleted file mode 100644 index 925a810a43..0000000000 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_followup_turn_trims_context_updates.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: core/tests/suite/compact_resume_fork.rs -expression: "context_snapshot::format_labeled_requests_snapshot(\"rollback trims pre-turn override context updates before the follow-up request\",\n&[(\"rolled-back turn request\", &requests[1]),\n(\"follow-up request after rollback\", &requests[2]),],\n&ContextSnapshotOptions::default().strip_capability_instructions().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 96 }),)" ---- -Scenario: rollback trims pre-turn override context updates before the follow-up request - -## rolled-back turn request -00:message/developer: -01:message/user:> -02:message/user:turn 1 user -03:message/assistant:turn 1 assistant -04:message/developer:ROLLED_BACK_DEV_INSTRUCTIONS -05:message/user: -06:message/user:turn 2 user - -## follow-up request after rollback -00:message/developer: -01:message/user:> -02:message/user:turn 1 user -03:message/assistant:turn 1 assistant -04:message/developer:ROLLED_BACK_DEV_INSTRUCTIONS -05:message/user: -06:message/user:follow-up user diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_past_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_past_compaction_shapes.snap deleted file mode 100644 index bb694af693..0000000000 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_past_compaction_shapes.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: core/tests/suite/compact_resume_fork.rs -expression: "context_snapshot::format_labeled_requests_snapshot(\"rollback past compaction replay after rollback\",\n&[(\"compaction request\", &requests[1]), (\"before rollback\", &requests[2]),\n(\"after rollback\", &requests[3]),],\n&ContextSnapshotOptions::default().strip_capability_instructions().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 64 }),)" ---- -Scenario: rollback past compaction replay after rollback - -## compaction request -00:message/developer: -01:message/user:> -02:message/user:hello world -03:message/assistant:FIRST_REPLY -04:message/user: - -## before rollback -00:message/user:hello world -01:message/user:\nSUMMARY_ONLY_CONTEXT -02:message/developer: -03:message/user:> -04:message/user:EDITED_AFTER_COMPACT - -## after rollback -00:message/user:hello world -01:message/user:\nSUMMARY_ONLY_CONTEXT -02:message/developer: -03:message/user:> -04:message/user:AFTER_ROLLBACK diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 81d799b615..8fb81c28b7 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -743,12 +743,6 @@ pub enum Op { /// model. SetThreadMemoryMode { mode: ThreadMemoryMode }, - /// Request Codex to drop the last N user turns from in-memory context. - /// - /// This does not attempt to revert local filesystem changes. Clients are - /// responsible for undoing any edits on disk. - ThreadRollback { num_turns: u32 }, - /// Request a code review from the agent. Review { review_request: ReviewRequest }, @@ -962,7 +956,6 @@ impl Op { Self::ReloadUserConfig => "reload_user_config", Self::Compact => "compact", Self::SetThreadMemoryMode { .. } => "set_thread_memory_mode", - Self::ThreadRollback { .. } => "thread_rollback", Self::Review { .. } => "review", Self::ApproveGuardianDeniedAction { .. } => "approve_guardian_denied_action", Self::Shutdown => "shutdown", @@ -1405,7 +1398,8 @@ pub enum EventMsg { /// Conversation history was compacted (either automatically or manually). ContextCompacted(ContextCompactedEvent), - /// Conversation history was rolled back by dropping the last N user turns. + /// Legacy persisted marker for dropping the last N user turns. + /// Retained for replay of existing rollouts; live rollback operations are unsupported. ThreadRolledBack(ThreadRolledBackEvent), /// Agent has started a turn. @@ -1888,6 +1882,7 @@ pub enum CodexErrorInfo { ActiveTurnNotSteerable { turn_kind: NonSteerableTurnKind, }, + // Retained to deserialize errors recorded in legacy rollouts. ThreadRollbackFailed, Other, } diff --git a/codex-rs/rollout/src/model_context.rs b/codex-rs/rollout/src/model_context.rs index 3ea48b8138..ba09120cdd 100644 --- a/codex-rs/rollout/src/model_context.rs +++ b/codex-rs/rollout/src/model_context.rs @@ -95,7 +95,7 @@ impl ModelContextScan { self.active_segment.saw_compaction = true; } RolloutItem::EventMsg(EventMsg::ThreadRolledBack(_)) => { - // Paginated threads reject rollback. Keep old rollouts correct rather than + // Rollback markers only occur in legacy history. Keep old rollouts correct rather than // duplicating rollback survival semantics in this bounded selector. self.must_scan_to_start = true; } diff --git a/codex-rs/thread-store/src/store.rs b/codex-rs/thread-store/src/store.rs index 64672c43f7..267df60076 100644 --- a/codex-rs/thread-store/src/store.rs +++ b/codex-rs/thread-store/src/store.rs @@ -144,7 +144,7 @@ pub trait ThreadStore: Any + Send + Sync { /// already-durable thread data. fn discard_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>; - /// Loads persisted history for resume, fork, rollback, and memory jobs. + /// Loads persisted history for resume, fork, and memory jobs. fn load_history( &self, params: LoadThreadHistoryParams, diff --git a/codex-rs/thread-store/src/types.rs b/codex-rs/thread-store/src/types.rs index 67105ae595..28568b3e2b 100644 --- a/codex-rs/thread-store/src/types.rs +++ b/codex-rs/thread-store/src/types.rs @@ -153,7 +153,7 @@ pub struct AppendThreadItemsParams { pub items: Vec, } -/// Parameters for loading persisted history for resume, fork, rollback, and memory jobs. +/// Parameters for loading persisted history for resume, fork, and memory jobs. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct LoadThreadHistoryParams { /// Thread id to load. diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 85c070e1ae..a983f2734a 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -607,8 +607,7 @@ pub(crate) struct App { pub(crate) backtrack: crate::app_backtrack::BacktrackState, /// When set, the next draw rebuilds terminal scrollback from the retained transcript cells. /// - /// This is used after a confirmed thread rollback to ensure scrollback reflects the trimmed - /// transcript cells. + /// This keeps scrollback consistent with the retained transcript after backtracking. pub(crate) backtrack_render_pending: bool, pub(crate) feedback: codex_feedback::CodexFeedback, feedback_audience: FeedbackAudience, diff --git a/codex-rs/tui/src/pager_overlay.rs b/codex-rs/tui/src/pager_overlay.rs index fb9cbf287f..09b9e6114b 100644 --- a/codex-rs/tui/src/pager_overlay.rs +++ b/codex-rs/tui/src/pager_overlay.rs @@ -695,7 +695,7 @@ impl TranscriptOverlay { /// Replace committed transcript cells while keeping any cached in-progress output that is /// currently shown at the end of the overlay. /// - /// This is used when existing history is trimmed (for example after rollback) so the + /// This is used when existing history is replaced or trimmed so the /// transcript overlay immediately reflects the same committed cells as the main transcript. pub(crate) fn replace_cells(&mut self, cells: Vec>) { let follow_bottom = self.view.is_scrolled_to_bottom(); diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index f5e248e124..0d8184b30c 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -5833,21 +5833,6 @@ class ThreadRevertedNotification(BaseModel): thread_id: Annotated[str, Field(alias="threadId")] -class ThreadRollbackParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - num_turns: Annotated[ - int, - Field( - alias="numTurns", - description="The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.", - ge=0, - ), - ] - thread_id: Annotated[str, Field(alias="threadId")] - - class ThreadSearchSortKey(Enum): created_at = "created_at" updated_at = "updated_at" @@ -6815,15 +6800,6 @@ class ThreadApproveGuardianDeniedActionRequest(BaseModel): params: ThreadApproveGuardianDeniedActionParams -class ThreadRollbackRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[Literal["thread/rollback"], Field(title="Thread/rollbackRequestMethod")] - params: ThreadRollbackParams - - class ThreadRevertRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -11767,7 +11743,7 @@ class Thread(BaseModel): turns: Annotated[ list[Turn], Field( - description="Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list." + description="Only populated on `thread/resume`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list." ), ] updated_at: Annotated[ @@ -11934,18 +11910,6 @@ class ThreadRevertResponse(BaseModel): ] = None -class ThreadRollbackResponse(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - thread: Annotated[ - Thread, - Field( - description="The updated thread after applying the rollback, with `turns` populated.\n\nThe ThreadItems stored in each Turn are lossy since we explicitly do not persist all agent interactions, such as command executions. This is the same behavior as `thread/resume`." - ), - ] - - class ThreadSearchResult(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -12169,7 +12133,6 @@ class ClientRequest( | ThreadCompactStartRequest | ThreadShellCommandRequest | ThreadApproveGuardianDeniedActionRequest - | ThreadRollbackRequest | ThreadRevertRequest | ThreadListRequest | ThreadSectionListRequest @@ -12277,7 +12240,6 @@ class ClientRequest( | ThreadCompactStartRequest | ThreadShellCommandRequest | ThreadApproveGuardianDeniedActionRequest - | ThreadRollbackRequest | ThreadRevertRequest | ThreadListRequest | ThreadSectionListRequest