diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 89ed9d8b13..35f5b33bcb 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -4119,6 +4119,74 @@ ], "type": "object" }, + "ThreadAttachmentAddParams": { + "description": "Parameters for creating or locating an attachment on its owning thread.", + "properties": { + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "payload": true, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentType", + "identityKey", + "payload", + "threadId" + ], + "type": "object" + }, + "ThreadAttachmentListParams": { + "description": "Parameters for listing attachments from one thread.", + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "ThreadAttachmentRemoveParams": { + "description": "Parameters for deleting an attachment by its stable thread-local identity.", + "properties": { + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentType", + "identityKey", + "threadId" + ], + "type": "object" + }, "ThreadCompactStartParams": { "properties": { "threadId": { @@ -6055,6 +6123,78 @@ "title": "Thread/metadata/updateRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/attachment/add" + ], + "title": "Thread/attachment/addRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadAttachmentAddParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/attachment/addRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/attachment/list" + ], + "title": "Thread/attachment/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadAttachmentListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/attachment/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/attachment/remove" + ], + "title": "Thread/attachment/removeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadAttachmentRemoveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/attachment/removeRequest", + "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 c99f2e9476..48efe6aed4 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -4323,6 +4323,42 @@ ], "type": "object" }, + "ThreadAttachmentOperation": { + "description": "The persisted attachment change represented by a notification.", + "enum": [ + "created", + "deleted" + ], + "type": "string" + }, + "ThreadAttachmentUpdatedNotification": { + "description": "Notification published after a thread attachment is created or deleted.", + "properties": { + "attachmentId": { + "type": "string" + }, + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "operation": { + "$ref": "#/definitions/ThreadAttachmentOperation" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentId", + "attachmentType", + "identityKey", + "operation", + "threadId" + ], + "type": "object" + }, "ThreadClosedNotification": { "properties": { "threadId": { @@ -6942,6 +6978,26 @@ "title": "Thread/name/updatedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/attachment/updated" + ], + "title": "Thread/attachment/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadAttachmentUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/attachment/updatedNotification", + "type": "object" + }, { "properties": { "method": { 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 0a4acaadaa..fbc8219570 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 @@ -492,6 +492,78 @@ "title": "Thread/metadata/updateRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "thread/attachment/add" + ], + "title": "Thread/attachment/addRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadAttachmentAddParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/attachment/addRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "thread/attachment/list" + ], + "title": "Thread/attachment/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadAttachmentListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/attachment/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "thread/attachment/remove" + ], + "title": "Thread/attachment/removeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadAttachmentRemoveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/attachment/removeRequest", + "type": "object" + }, { "properties": { "id": { @@ -4678,6 +4750,26 @@ "title": "Thread/name/updatedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/attachment/updated" + ], + "title": "Thread/attachment/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadAttachmentUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/attachment/updatedNotification", + "type": "object" + }, { "properties": { "method": { @@ -20553,6 +20645,200 @@ "title": "ThreadArchivedNotification", "type": "object" }, + "ThreadAttachment": { + "description": "An independently persisted attachment associated with a thread.", + "properties": { + "attachmentType": { + "type": "string" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "id": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "payload": true + }, + "required": [ + "attachmentType", + "createdAt", + "id", + "identityKey", + "payload" + ], + "type": "object" + }, + "ThreadAttachmentAddOutcome": { + "description": "Result of attempting to associate an attachment with a thread.", + "enum": [ + "created", + "existing" + ], + "type": "string" + }, + "ThreadAttachmentAddParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for creating or locating an attachment on its owning thread.", + "properties": { + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "payload": true, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentType", + "identityKey", + "payload", + "threadId" + ], + "title": "ThreadAttachmentAddParams", + "type": "object" + }, + "ThreadAttachmentAddResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "The created or existing attachment.", + "properties": { + "attachment": { + "$ref": "#/definitions/v2/ThreadAttachment" + }, + "outcome": { + "$ref": "#/definitions/v2/ThreadAttachmentAddOutcome" + } + }, + "required": [ + "attachment", + "outcome" + ], + "title": "ThreadAttachmentAddResponse", + "type": "object" + }, + "ThreadAttachmentListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for listing attachments from one thread.", + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadAttachmentListParams", + "type": "object" + }, + "ThreadAttachmentListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "One page of attachments associated with the requested thread.", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/v2/ThreadAttachment" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ThreadAttachmentListResponse", + "type": "object" + }, + "ThreadAttachmentOperation": { + "description": "The persisted attachment change represented by a notification.", + "enum": [ + "created", + "deleted" + ], + "type": "string" + }, + "ThreadAttachmentRemoveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for deleting an attachment by its stable thread-local identity.", + "properties": { + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentType", + "identityKey", + "threadId" + ], + "title": "ThreadAttachmentRemoveParams", + "type": "object" + }, + "ThreadAttachmentRemoveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful deletion does not return additional attachment data.", + "title": "ThreadAttachmentRemoveResponse", + "type": "object" + }, + "ThreadAttachmentUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Notification published after a thread attachment is created or deleted.", + "properties": { + "attachmentId": { + "type": "string" + }, + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "operation": { + "$ref": "#/definitions/v2/ThreadAttachmentOperation" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentId", + "attachmentType", + "identityKey", + "operation", + "threadId" + ], + "title": "ThreadAttachmentUpdatedNotification", + "type": "object" + }, "ThreadClosedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { 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 38d0b00fe5..ffd953c165 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 @@ -2022,6 +2022,78 @@ "title": "Thread/metadata/updateRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/attachment/add" + ], + "title": "Thread/attachment/addRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadAttachmentAddParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/attachment/addRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/attachment/list" + ], + "title": "Thread/attachment/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadAttachmentListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/attachment/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/attachment/remove" + ], + "title": "Thread/attachment/removeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadAttachmentRemoveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/attachment/removeRequest", + "type": "object" + }, { "properties": { "id": { @@ -15819,6 +15891,26 @@ "title": "Thread/name/updatedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/attachment/updated" + ], + "title": "Thread/attachment/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadAttachmentUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/attachment/updatedNotification", + "type": "object" + }, { "properties": { "method": { @@ -18267,6 +18359,200 @@ "title": "ThreadArchivedNotification", "type": "object" }, + "ThreadAttachment": { + "description": "An independently persisted attachment associated with a thread.", + "properties": { + "attachmentType": { + "type": "string" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "id": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "payload": true + }, + "required": [ + "attachmentType", + "createdAt", + "id", + "identityKey", + "payload" + ], + "type": "object" + }, + "ThreadAttachmentAddOutcome": { + "description": "Result of attempting to associate an attachment with a thread.", + "enum": [ + "created", + "existing" + ], + "type": "string" + }, + "ThreadAttachmentAddParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for creating or locating an attachment on its owning thread.", + "properties": { + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "payload": true, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentType", + "identityKey", + "payload", + "threadId" + ], + "title": "ThreadAttachmentAddParams", + "type": "object" + }, + "ThreadAttachmentAddResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "The created or existing attachment.", + "properties": { + "attachment": { + "$ref": "#/definitions/ThreadAttachment" + }, + "outcome": { + "$ref": "#/definitions/ThreadAttachmentAddOutcome" + } + }, + "required": [ + "attachment", + "outcome" + ], + "title": "ThreadAttachmentAddResponse", + "type": "object" + }, + "ThreadAttachmentListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for listing attachments from one thread.", + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadAttachmentListParams", + "type": "object" + }, + "ThreadAttachmentListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "One page of attachments associated with the requested thread.", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/ThreadAttachment" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ThreadAttachmentListResponse", + "type": "object" + }, + "ThreadAttachmentOperation": { + "description": "The persisted attachment change represented by a notification.", + "enum": [ + "created", + "deleted" + ], + "type": "string" + }, + "ThreadAttachmentRemoveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for deleting an attachment by its stable thread-local identity.", + "properties": { + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentType", + "identityKey", + "threadId" + ], + "title": "ThreadAttachmentRemoveParams", + "type": "object" + }, + "ThreadAttachmentRemoveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful deletion does not return additional attachment data.", + "title": "ThreadAttachmentRemoveResponse", + "type": "object" + }, + "ThreadAttachmentUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Notification published after a thread attachment is created or deleted.", + "properties": { + "attachmentId": { + "type": "string" + }, + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "operation": { + "$ref": "#/definitions/ThreadAttachmentOperation" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentId", + "attachmentType", + "identityKey", + "operation", + "threadId" + ], + "title": "ThreadAttachmentUpdatedNotification", + "type": "object" + }, "ThreadClosedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentAddParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentAddParams.json new file mode 100644 index 0000000000..9bb4373cd1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentAddParams.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for creating or locating an attachment on its owning thread.", + "properties": { + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "payload": true, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentType", + "identityKey", + "payload", + "threadId" + ], + "title": "ThreadAttachmentAddParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentAddResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentAddResponse.json new file mode 100644 index 0000000000..a233fa7b67 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentAddResponse.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ThreadAttachment": { + "description": "An independently persisted attachment associated with a thread.", + "properties": { + "attachmentType": { + "type": "string" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "id": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "payload": true + }, + "required": [ + "attachmentType", + "createdAt", + "id", + "identityKey", + "payload" + ], + "type": "object" + }, + "ThreadAttachmentAddOutcome": { + "description": "Result of attempting to associate an attachment with a thread.", + "enum": [ + "created", + "existing" + ], + "type": "string" + } + }, + "description": "The created or existing attachment.", + "properties": { + "attachment": { + "$ref": "#/definitions/ThreadAttachment" + }, + "outcome": { + "$ref": "#/definitions/ThreadAttachmentAddOutcome" + } + }, + "required": [ + "attachment", + "outcome" + ], + "title": "ThreadAttachmentAddResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentListParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentListParams.json new file mode 100644 index 0000000000..81ac3ee804 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentListParams.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for listing attachments from one thread.", + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadAttachmentListParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentListResponse.json new file mode 100644 index 0000000000..e7bc96760c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentListResponse.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ThreadAttachment": { + "description": "An independently persisted attachment associated with a thread.", + "properties": { + "attachmentType": { + "type": "string" + }, + "createdAt": { + "format": "int64", + "type": "integer" + }, + "id": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "payload": true + }, + "required": [ + "attachmentType", + "createdAt", + "id", + "identityKey", + "payload" + ], + "type": "object" + } + }, + "description": "One page of attachments associated with the requested thread.", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/ThreadAttachment" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ThreadAttachmentListResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentRemoveParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentRemoveParams.json new file mode 100644 index 0000000000..d505382b0b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentRemoveParams.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for deleting an attachment by its stable thread-local identity.", + "properties": { + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentType", + "identityKey", + "threadId" + ], + "title": "ThreadAttachmentRemoveParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentRemoveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentRemoveResponse.json new file mode 100644 index 0000000000..515bbde3d2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentRemoveResponse.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful deletion does not return additional attachment data.", + "title": "ThreadAttachmentRemoveResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentUpdatedNotification.json new file mode 100644 index 0000000000..177163dbeb --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadAttachmentUpdatedNotification.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ThreadAttachmentOperation": { + "description": "The persisted attachment change represented by a notification.", + "enum": [ + "created", + "deleted" + ], + "type": "string" + } + }, + "description": "Notification published after a thread attachment is created or deleted.", + "properties": { + "attachmentId": { + "type": "string" + }, + "attachmentType": { + "type": "string" + }, + "identityKey": { + "type": "string" + }, + "operation": { + "$ref": "#/definitions/ThreadAttachmentOperation" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "attachmentId", + "attachmentType", + "identityKey", + "operation", + "threadId" + ], + "title": "ThreadAttachmentUpdatedNotification", + "type": "object" +} \ No newline at end of file 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 e21cebce6a..99c6ae3d6c 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 15d7e3619b..414537133b 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 0ccca6ce2c..e6366163cf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -68,6 +68,9 @@ import type { SkillsExtraRootsSetParams } from "./v2/SkillsExtraRootsSetParams"; import type { SkillsListParams } from "./v2/SkillsListParams"; import type { ThreadApproveGuardianDeniedActionParams } from "./v2/ThreadApproveGuardianDeniedActionParams"; import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams"; +import type { ThreadAttachmentAddParams } from "./v2/ThreadAttachmentAddParams"; +import type { ThreadAttachmentListParams } from "./v2/ThreadAttachmentListParams"; +import type { ThreadAttachmentRemoveParams } from "./v2/ThreadAttachmentRemoveParams"; import type { ThreadCompactStartParams } from "./v2/ThreadCompactStartParams"; import type { ThreadDeleteParams } from "./v2/ThreadDeleteParams"; import type { ThreadForkParams } from "./v2/ThreadForkParams"; @@ -102,4 +105,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/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/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, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index c65fccb789..da1360df17 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -50,6 +50,7 @@ import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification"; import type { StrictReviewRequiredNotification } from "./v2/StrictReviewRequiredNotification"; import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification"; +import type { ThreadAttachmentUpdatedNotification } from "./v2/ThreadAttachmentUpdatedNotification"; import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification"; import type { ThreadDeletedNotification } from "./v2/ThreadDeletedNotification"; import type { ThreadGoalClearedNotification } from "./v2/ThreadGoalClearedNotification"; @@ -86,4 +87,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/attachment/updated", "params": ThreadAttachmentUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts index bb5799a140..47f35db0c7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotificationEnvelope.ts @@ -50,6 +50,7 @@ import type { SkillsChangedNotification } from "./v2/SkillsChangedNotification"; import type { StrictReviewRequiredNotification } from "./v2/StrictReviewRequiredNotification"; import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification"; +import type { ThreadAttachmentUpdatedNotification } from "./v2/ThreadAttachmentUpdatedNotification"; import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification"; import type { ThreadDeletedNotification } from "./v2/ThreadDeletedNotification"; import type { ThreadGoalClearedNotification } from "./v2/ThreadGoalClearedNotification"; @@ -96,4 +97,4 @@ export type ServerNotificationEnvelope = { * Optional so clients can decode notifications from older app-server * versions. Current app-server versions always populate it. */ -emittedAtMs?: number, } & ({ "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }); +emittedAtMs?: number, } & ({ "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/deleted", "params": ThreadDeletedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/reverted", "params": ThreadRevertedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/attachment/updated", "params": ThreadAttachmentUpdatedNotification } | { "method": "thread/goal/updated", "params": ThreadGoalUpdatedNotification } | { "method": "thread/goal/cleared", "params": ThreadGoalClearedNotification } | { "method": "thread/queue/changed", "params": ThreadQueueChangedNotification } | { "method": "project/changed", "params": ProjectChangedNotification } | { "method": "thread/project/updated", "params": ThreadProjectUpdatedNotification } | { "method": "thread/environment/connected", "params": EnvironmentConnectionNotification } | { "method": "thread/environment/disconnected", "params": EnvironmentConnectionNotification } | { "method": "thread/settings/updated", "params": ThreadSettingsUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "autoApprovalReview/strictReviewRequired", "params": StrictReviewRequiredNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "rawResponse/completed", "params": RawResponseCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "process/outputDelta", "params": ProcessOutputDeltaNotification } | { "method": "process/exited", "params": ProcessExitedNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/fileChange/patchUpdated", "params": FileChangePatchUpdatedNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "mcpServer/event/stream/notification", "params": McpServerEventStreamNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "remoteControl/status/changed", "params": RemoteControlStatusChangedNotification } | { "method": "externalAgentConfig/import/progress", "params": ExternalAgentConfigImportProgressNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "model/verification", "params": ModelVerificationNotification } | { "method": "modelProvider/authRecoveryStarted", "params": AuthRecoveryNotification } | { "method": "modelProvider/authRecoveryCompleted", "params": AuthRecoveryNotification } | { "method": "turn/moderationMetadata", "params": TurnModerationMetadataNotification } | { "method": "model/safetyBuffering/updated", "params": ModelSafetyBufferingUpdatedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "guardianWarning", "params": GuardianWarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/item/started", "params": ThreadRealtimeItemStartedNotification } | { "method": "thread/realtime/item/transcript/delta", "params": ThreadRealtimeItemTranscriptDeltaNotification } | { "method": "thread/realtime/item/completed", "params": ThreadRealtimeItemCompletedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }); diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachment.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachment.ts new file mode 100644 index 0000000000..c8fefd0acd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachment.ts @@ -0,0 +1,9 @@ +// 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 { JsonValue } from "../serde_json/JsonValue"; + +/** + * An independently persisted attachment associated with a thread. + */ +export type ThreadAttachment = { id: string, attachmentType: string, identityKey: string, payload: JsonValue, createdAt: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentAddOutcome.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentAddOutcome.ts new file mode 100644 index 0000000000..4126e664db --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentAddOutcome.ts @@ -0,0 +1,8 @@ +// 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. + +/** + * Result of attempting to associate an attachment with a thread. + */ +export type ThreadAttachmentAddOutcome = "created" | "existing"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentAddParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentAddParams.ts new file mode 100644 index 0000000000..6fa166ad6b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentAddParams.ts @@ -0,0 +1,9 @@ +// 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 { JsonValue } from "../serde_json/JsonValue"; + +/** + * Parameters for creating or locating an attachment on its owning thread. + */ +export type ThreadAttachmentAddParams = { threadId: string, attachmentType: string, identityKey: string, payload: JsonValue, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentAddResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentAddResponse.ts new file mode 100644 index 0000000000..e43e642aab --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentAddResponse.ts @@ -0,0 +1,10 @@ +// 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 { ThreadAttachment } from "./ThreadAttachment"; +import type { ThreadAttachmentAddOutcome } from "./ThreadAttachmentAddOutcome"; + +/** + * The created or existing attachment. + */ +export type ThreadAttachmentAddResponse = { outcome: ThreadAttachmentAddOutcome, attachment: ThreadAttachment, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentListParams.ts new file mode 100644 index 0000000000..2946b32b49 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentListParams.ts @@ -0,0 +1,8 @@ +// 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. + +/** + * Parameters for listing attachments from one thread. + */ +export type ThreadAttachmentListParams = { threadId: string, cursor?: string | null, limit?: number | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentListResponse.ts new file mode 100644 index 0000000000..ad8ed1759e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentListResponse.ts @@ -0,0 +1,9 @@ +// 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 { ThreadAttachment } from "./ThreadAttachment"; + +/** + * One page of attachments associated with the requested thread. + */ +export type ThreadAttachmentListResponse = { data: Array, nextCursor: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentOperation.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentOperation.ts new file mode 100644 index 0000000000..28503137e7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentOperation.ts @@ -0,0 +1,8 @@ +// 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. + +/** + * The persisted attachment change represented by a notification. + */ +export type ThreadAttachmentOperation = "created" | "deleted"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentRemoveParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentRemoveParams.ts new file mode 100644 index 0000000000..c43fdc2324 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentRemoveParams.ts @@ -0,0 +1,8 @@ +// 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. + +/** + * Parameters for deleting an attachment by its stable thread-local identity. + */ +export type ThreadAttachmentRemoveParams = { threadId: string, attachmentType: string, identityKey: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentRemoveResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentRemoveResponse.ts new file mode 100644 index 0000000000..d506e02c38 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentRemoveResponse.ts @@ -0,0 +1,8 @@ +// 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. + +/** + * Successful deletion does not return additional attachment data. + */ +export type ThreadAttachmentRemoveResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentUpdatedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentUpdatedNotification.ts new file mode 100644 index 0000000000..8cfe104f01 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadAttachmentUpdatedNotification.ts @@ -0,0 +1,9 @@ +// 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 { ThreadAttachmentOperation } from "./ThreadAttachmentOperation"; + +/** + * Notification published after a thread attachment is created or deleted. + */ +export type ThreadAttachmentUpdatedNotification = { threadId: string, attachmentType: string, identityKey: string, attachmentId: string, operation: ThreadAttachmentOperation, }; 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 a3595e7769..e3df4541a4 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -469,6 +469,16 @@ export type { ThreadApproveGuardianDeniedActionResponse } from "./ThreadApproveG export type { ThreadArchiveParams } from "./ThreadArchiveParams"; export type { ThreadArchiveResponse } from "./ThreadArchiveResponse"; export type { ThreadArchivedNotification } from "./ThreadArchivedNotification"; +export type { ThreadAttachment } from "./ThreadAttachment"; +export type { ThreadAttachmentAddOutcome } from "./ThreadAttachmentAddOutcome"; +export type { ThreadAttachmentAddParams } from "./ThreadAttachmentAddParams"; +export type { ThreadAttachmentAddResponse } from "./ThreadAttachmentAddResponse"; +export type { ThreadAttachmentListParams } from "./ThreadAttachmentListParams"; +export type { ThreadAttachmentListResponse } from "./ThreadAttachmentListResponse"; +export type { ThreadAttachmentOperation } from "./ThreadAttachmentOperation"; +export type { ThreadAttachmentRemoveParams } from "./ThreadAttachmentRemoveParams"; +export type { ThreadAttachmentRemoveResponse } from "./ThreadAttachmentRemoveResponse"; +export type { ThreadAttachmentUpdatedNotification } from "./ThreadAttachmentUpdatedNotification"; export type { ThreadClosedNotification } from "./ThreadClosedNotification"; export type { ThreadCompactStartParams } from "./ThreadCompactStartParams"; export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 947f2bfb0b..70954a3d60 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -670,6 +670,21 @@ client_request_definitions! { serialization: thread_id(params.thread_id), response: v2::ThreadMetadataUpdateResponse, }, + ThreadAttachmentAdd => "thread/attachment/add" { + params: v2::ThreadAttachmentAddParams, + serialization: thread_id(params.thread_id), + response: v2::ThreadAttachmentAddResponse, + }, + ThreadAttachmentList => "thread/attachment/list" { + params: v2::ThreadAttachmentListParams, + serialization: None, + response: v2::ThreadAttachmentListResponse, + }, + ThreadAttachmentRemove => "thread/attachment/remove" { + params: v2::ThreadAttachmentRemoveParams, + serialization: thread_id(params.thread_id), + response: v2::ThreadAttachmentRemoveResponse, + }, ThreadSectionMove => "thread/section/move" { params: v2::ThreadSectionMoveParams, serialization: thread_id(params.thread_id), @@ -1901,6 +1916,7 @@ server_notification_definitions! { ThreadReverted => "thread/reverted" (v2::ThreadRevertedNotification), SkillsChanged => "skills/changed" (v2::SkillsChangedNotification), ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification), + ThreadAttachmentUpdated => "thread/attachment/updated" (v2::ThreadAttachmentUpdatedNotification), ThreadGoalUpdated => "thread/goal/updated" (v2::ThreadGoalUpdatedNotification), ThreadGoalCleared => "thread/goal/cleared" (v2::ThreadGoalClearedNotification), #[experimental("thread/queue/changed")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs index 736c577706..518c1b5227 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs @@ -31,6 +31,7 @@ mod realtime; mod remote_control; mod review; mod thread; +mod thread_attachment; mod thread_data; mod thread_usage; mod turn; @@ -69,6 +70,7 @@ pub use remote_control::*; pub use review::*; pub use shared::*; pub use thread::*; +pub use thread_attachment::*; pub use thread_data::*; pub use thread_usage::*; pub use turn::*; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_attachment.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_attachment.rs new file mode 100644 index 0000000000..ed8c5766b2 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_attachment.rs @@ -0,0 +1,107 @@ +//! Wire contracts for managing the current set of attachments on a thread. + +use crate::JsonSchema; +use crate::TS; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; + +/// An independently persisted attachment associated with a thread. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadAttachment { + pub id: String, + pub attachment_type: String, + pub identity_key: String, + pub payload: JsonValue, + #[ts(type = "number")] + pub created_at: i64, +} + +/// Parameters for creating or locating an attachment on its owning thread. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadAttachmentAddParams { + pub thread_id: String, + pub attachment_type: String, + pub identity_key: String, + pub payload: JsonValue, +} + +/// Result of attempting to associate an attachment with a thread. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum ThreadAttachmentAddOutcome { + Created, + Existing, +} + +/// The created or existing attachment. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadAttachmentAddResponse { + pub outcome: ThreadAttachmentAddOutcome, + pub attachment: ThreadAttachment, +} + +/// Parameters for listing attachments from one thread. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadAttachmentListParams { + pub thread_id: String, + #[ts(optional = nullable)] + pub cursor: Option, + #[ts(optional = nullable)] + pub limit: Option, +} + +/// One page of attachments associated with the requested thread. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadAttachmentListResponse { + pub data: Vec, + pub next_cursor: Option, +} + +/// Parameters for deleting an attachment by its stable thread-local identity. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadAttachmentRemoveParams { + pub thread_id: String, + pub attachment_type: String, + pub identity_key: String, +} + +/// Successful deletion does not return additional attachment data. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadAttachmentRemoveResponse {} + +/// The persisted attachment change represented by a notification. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum ThreadAttachmentOperation { + Created, + Deleted, +} + +/// Notification published after a thread attachment is created or deleted. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadAttachmentUpdatedNotification { + pub thread_id: String, + pub attachment_type: String, + pub identity_key: String, + pub attachment_id: String, + pub operation: ThreadAttachmentOperation, +} diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a8558fc127..d64c2303f9 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -50,3 +50,66 @@ If `model_providers.amazon-bedrock.aws.credential_export` is configured, Bedrock Bedrock login return an error without changing configuration or saved credentials. Remove the exporter configuration before selecting another credential source. `aws.credential_export` and `aws.profile` cannot be configured together. + +## Stored thread attachments + +- `thread/attachment/add` — add a durable resource reference to a stored thread without loading it. Repeated writes with the same attachment type and identity key return the existing attachment. +- `thread/attachment/list` — list attachments for one stored thread in a cursor-paginated request, including a thread that is not loaded. +- `thread/attachment/remove` — remove an attachment by its thread, attachment type, and identity key; returns `{}`. +- `thread/attachment/updated` — notification broadcast after an attachment is created or removed; contains the thread, attachment identity, attachment id, and operation. +### Example: Manage stored thread attachments + +Attachments record the resources currently associated with a thread, independently of conversation history. Clients can add, remove, and list attachments for one stored thread at a time without resuming those threads. Adding or removing an attachment does not create or delete the underlying resource or rewrite history. An attachment is idempotently identified by its thread, `attachmentType`, and `identityKey`. For pull requests, clients should reuse the canonical application identity `JSON.stringify([canonicalHostname, lowercaseOwner, lowercaseRepository, pullRequestNumber])` so addition and removal agree across surfaces. + +```json +{ "method": "thread/attachment/add", "id": 20, "params": { + "threadId": "thr_123", + "attachmentType": "pull_request", + "identityKey": "[\"github.com\",\"openai\",\"codex\",123]", + "payload": { "url": "https://github.com/openai/codex/pull/123" } +} } +{ "id": 20, "result": { + "outcome": "created", + "attachment": { + "id": "01984de2-8f74-7c91-a3b2-5c5e937cf318", + "attachmentType": "pull_request", + "identityKey": "[\"github.com\",\"openai\",\"codex\",123]", + "payload": { "url": "https://github.com/openai/codex/pull/123" }, + "createdAt": 1750000000 + } +} } + +{ "method": "thread/attachment/list", "id": 21, "params": { + "threadId": "thr_123", + "limit": 100 +} } +{ "id": 21, "result": { + "data": [{ + "id": "01984de2-8f74-7c91-a3b2-5c5e937cf318", + "attachmentType": "pull_request", + "identityKey": "[\"github.com\",\"openai\",\"codex\",123]", + "payload": { "url": "https://github.com/openai/codex/pull/123" }, + "createdAt": 1750000000 + }], + "nextCursor": null +} } + +{ "method": "thread/attachment/remove", "id": 22, "params": { + "threadId": "thr_123", + "attachmentType": "pull_request", + "identityKey": "[\"github.com\",\"openai\",\"codex\",123]" +} } +{ "id": 22, "result": {} } + +{ "method": "thread/attachment/updated", "params": { + "threadId": "thr_123", + "attachmentType": "pull_request", + "identityKey": "[\"github.com\",\"openai\",\"codex\",123]", + "attachmentId": "01984de2-8f74-7c91-a3b2-5c5e937cf318", + "operation": "deleted" +} } +``` + +`thread/attachment/list` accepts one `threadId` and returns at most 100 attachments per page, ordered by creation time and attachment id. Continue with `nextCursor` and the same `threadId` until the cursor is `null`. Each thread can retain up to 100 attachments. Removing an attachment frees a slot for a new attachment. + +Attachment creation and deletion requests using the same thread ID are serialized across connections. The requesting client receives its response before the compact update is broadcast, and duplicate creates or absent deletes do not emit updates. Deleting the owning thread removes its attachments under the same lifecycle exclusion; queued attachment mutations then report that the thread was not found. diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index 65a7d4e1db..20e44d3253 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -114,6 +114,7 @@ fn server_notification_requires_delivery(notification: &ServerNotification) -> b ServerNotification::TurnCompleted(_) | ServerNotification::ThreadQueueChanged(_) | ServerNotification::ThreadSettingsUpdated(_) + | ServerNotification::ThreadAttachmentUpdated(_) | ServerNotification::ExternalAgentConfigImportCompleted(_) | ServerNotification::ItemCompleted(ItemCompletedNotification { item: ThreadItem::AgentMessage { @@ -803,6 +804,8 @@ mod tests { use codex_app_server_protocol::ConfigRequirementsReadResponse; use codex_app_server_protocol::ExternalAgentConfigImportCompletedNotification; use codex_app_server_protocol::SessionSource as ApiSessionSource; + use codex_app_server_protocol::ThreadAttachmentOperation; + use codex_app_server_protocol::ThreadAttachmentUpdatedNotification; use codex_app_server_protocol::ThreadQueueChangedNotification; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; @@ -1004,7 +1007,7 @@ mod tests { } #[test] - fn guaranteed_delivery_helpers_cover_terminal_server_notifications() { + fn guaranteed_delivery_helpers_cover_required_server_notifications() { assert!(server_notification_requires_delivery( &ServerNotification::TurnCompleted(TurnCompletedNotification { thread_id: "thread-1".to_string(), @@ -1033,6 +1036,15 @@ mod tests { }, ) )); + assert!(server_notification_requires_delivery( + &ServerNotification::ThreadAttachmentUpdated(ThreadAttachmentUpdatedNotification { + thread_id: "thread-1".to_string(), + attachment_type: "pull_request".to_string(), + identity_key: r#"["github.com","openai","codex",123]"#.to_string(), + attachment_id: "attachment-1".to_string(), + operation: ThreadAttachmentOperation::Deleted, + }) + )); assert!(server_notification_requires_delivery( &ServerNotification::ItemCompleted(ItemCompletedNotification { item: ThreadItem::AgentMessage { diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 040192c75b..be62b731bf 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1384,6 +1384,19 @@ impl MessageProcessor { ClientRequest::ThreadMetadataUpdate { params, .. } => { self.thread_processor.thread_metadata_update(params).await } + ClientRequest::ThreadAttachmentAdd { params, .. } => { + self.thread_processor + .thread_attachment_add(request_id.clone(), params) + .await + } + ClientRequest::ThreadAttachmentList { params, .. } => { + self.thread_processor.thread_attachment_list(params).await + } + ClientRequest::ThreadAttachmentRemove { params, .. } => { + self.thread_processor + .thread_attachment_remove(request_id.clone(), params) + .await + } ClientRequest::ThreadSectionMove { params, .. } => { self.thread_processor.thread_section_move(params).await } diff --git a/codex-rs/app-server/src/notification_media.rs b/codex-rs/app-server/src/notification_media.rs index 9fca0f3e42..04e57f8c53 100644 --- a/codex-rs/app-server/src/notification_media.rs +++ b/codex-rs/app-server/src/notification_media.rs @@ -64,6 +64,7 @@ pub(crate) fn without_notification_media(notification: ServerNotification) -> Se | ServerNotification::ThreadReverted(_) | ServerNotification::SkillsChanged(_) | ServerNotification::ThreadNameUpdated(_) + | ServerNotification::ThreadAttachmentUpdated(_) | ServerNotification::ThreadGoalUpdated(_) | ServerNotification::ThreadGoalCleared(_) | ServerNotification::ThreadQueueChanged(_) diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index a79759d9d0..6ec3567f2f 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -559,6 +559,7 @@ mod process_exec_processor; mod projects; mod remote_control_processor; mod search; +mod thread_attachments; mod thread_enrichment; mod thread_fork_goal; mod thread_input; diff --git a/codex-rs/app-server/src/request_processors/thread_attachments.rs b/codex-rs/app-server/src/request_processors/thread_attachments.rs new file mode 100644 index 0000000000..ce19d06db9 --- /dev/null +++ b/codex-rs/app-server/src/request_processors/thread_attachments.rs @@ -0,0 +1,243 @@ +//! Attachment RPC handling; committed mutations respond before broadcasting updates. + +use super::thread_processor::THREAD_LIST_DEFAULT_LIMIT; +use super::thread_processor::ThreadRequestProcessor; +use crate::error_code::internal_error; +use crate::error_code::invalid_params; +use crate::error_code::method_not_found; +use crate::outgoing_message::ConnectionRequestId; +use codex_app_server_protocol::ClientResponsePayload; +use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::ThreadAttachment; +use codex_app_server_protocol::ThreadAttachmentAddOutcome; +use codex_app_server_protocol::ThreadAttachmentAddParams; +use codex_app_server_protocol::ThreadAttachmentAddResponse; +use codex_app_server_protocol::ThreadAttachmentListParams; +use codex_app_server_protocol::ThreadAttachmentListResponse; +use codex_app_server_protocol::ThreadAttachmentOperation; +use codex_app_server_protocol::ThreadAttachmentRemoveParams; +use codex_app_server_protocol::ThreadAttachmentRemoveResponse; +use codex_app_server_protocol::ThreadAttachmentUpdatedNotification; +use codex_protocol::ThreadId; +use codex_state::MAX_THREAD_ATTACHMENT_IDENTITY_KEY_BYTES; +use codex_state::MAX_THREAD_ATTACHMENT_LIST_PAGE_SIZE; +use codex_state::MAX_THREAD_ATTACHMENT_PAYLOAD_BYTES; +use codex_state::MAX_THREAD_ATTACHMENT_TYPE_BYTES; +use codex_thread_store::AddThreadAttachmentOutcome; +use codex_thread_store::AddThreadAttachmentParams; +use codex_thread_store::ListThreadAttachmentsParams; +use codex_thread_store::RemoveThreadAttachmentOutcome; +use codex_thread_store::RemoveThreadAttachmentParams; +use codex_thread_store::ThreadAttachment as StoredThreadAttachment; +use codex_thread_store::ThreadStoreError; + +impl ThreadRequestProcessor { + pub(crate) async fn thread_attachment_add( + &self, + request_id: ConnectionRequestId, + params: ThreadAttachmentAddParams, + ) -> Result, JSONRPCErrorError> { + const OPERATION: &str = "thread/attachment/add"; + self.ensure_thread_attachments_supported(OPERATION)?; + validate_attachment_identity(¶ms.attachment_type, ¶ms.identity_key)?; + let payload_bytes = serde_json::to_vec(¶ms.payload) + .map_err(|error| invalid_params(format!("invalid attachment payload: {error}")))?; + if payload_bytes.len() > MAX_THREAD_ATTACHMENT_PAYLOAD_BYTES { + return Err(invalid_params(format!( + "attachment payload must not exceed {MAX_THREAD_ATTACHMENT_PAYLOAD_BYTES} bytes" + ))); + } + + let thread_id = parse_attachment_thread_id(¶ms.thread_id)?; + let result = self + .thread_store + .add_thread_attachment(AddThreadAttachmentParams { + thread_id, + attachment_type: params.attachment_type, + identity_key: params.identity_key, + payload: params.payload, + }) + .await + .map_err(|error| thread_attachment_store_error(OPERATION, error))?; + + let (response, notification) = match result { + AddThreadAttachmentOutcome::Created(attachment) => { + let notification = ThreadAttachmentUpdatedNotification { + thread_id: attachment.thread_id.to_string(), + attachment_type: attachment.attachment_type.clone(), + identity_key: attachment.identity_key.clone(), + attachment_id: attachment.id.clone(), + operation: ThreadAttachmentOperation::Created, + }; + ( + ThreadAttachmentAddResponse { + outcome: ThreadAttachmentAddOutcome::Created, + attachment: api_thread_attachment(attachment), + }, + Some(notification), + ) + } + AddThreadAttachmentOutcome::Existing(attachment) => ( + ThreadAttachmentAddResponse { + outcome: ThreadAttachmentAddOutcome::Existing, + attachment: api_thread_attachment(attachment), + }, + None, + ), + }; + + self.outgoing.send_response(request_id, response).await; + if let Some(notification) = notification { + self.outgoing + .send_server_notification(ServerNotification::ThreadAttachmentUpdated(notification)) + .await; + } + Ok(None) + } + + pub(crate) async fn thread_attachment_list( + &self, + params: ThreadAttachmentListParams, + ) -> Result, JSONRPCErrorError> { + const OPERATION: &str = "thread/attachment/list"; + self.ensure_thread_attachments_supported(OPERATION)?; + let thread_id = parse_attachment_thread_id(¶ms.thread_id)?; + let limit = params + .limit + .map(|limit| limit as usize) + .unwrap_or(THREAD_LIST_DEFAULT_LIMIT) + .clamp(1, MAX_THREAD_ATTACHMENT_LIST_PAGE_SIZE); + let page = self + .thread_store + .list_thread_attachments(ListThreadAttachmentsParams { + thread_id, + cursor: params.cursor, + limit, + }) + .await + .map_err(|error| thread_attachment_store_error(OPERATION, error))?; + + Ok(Some( + ThreadAttachmentListResponse { + data: page + .attachments + .into_iter() + .map(api_thread_attachment) + .collect(), + next_cursor: page.next_cursor, + } + .into(), + )) + } + + pub(crate) async fn thread_attachment_remove( + &self, + request_id: ConnectionRequestId, + params: ThreadAttachmentRemoveParams, + ) -> Result, JSONRPCErrorError> { + const OPERATION: &str = "thread/attachment/remove"; + self.ensure_thread_attachments_supported(OPERATION)?; + validate_attachment_identity(¶ms.attachment_type, ¶ms.identity_key)?; + let thread_id = parse_attachment_thread_id(¶ms.thread_id)?; + let result = self + .thread_store + .remove_thread_attachment(RemoveThreadAttachmentParams { + thread_id, + attachment_type: params.attachment_type, + identity_key: params.identity_key, + }) + .await + .map_err(|error| thread_attachment_store_error(OPERATION, error))?; + + self.outgoing + .send_response(request_id, ThreadAttachmentRemoveResponse {}) + .await; + if let RemoveThreadAttachmentOutcome::Removed(attachment) = result { + self.outgoing + .send_server_notification(ServerNotification::ThreadAttachmentUpdated( + ThreadAttachmentUpdatedNotification { + thread_id: attachment.thread_id.to_string(), + attachment_type: attachment.attachment_type, + identity_key: attachment.identity_key, + attachment_id: attachment.id, + operation: ThreadAttachmentOperation::Deleted, + }, + )) + .await; + } + + Ok(None) + } + + fn ensure_thread_attachments_supported( + &self, + operation: &'static str, + ) -> Result<(), JSONRPCErrorError> { + if self.thread_store.supports_thread_attachments() { + Ok(()) + } else { + Err(unsupported_thread_attachment_operation(operation)) + } + } +} + +fn validate_attachment_identity( + attachment_type: &str, + identity_key: &str, +) -> Result<(), JSONRPCErrorError> { + if attachment_type.trim().is_empty() { + return Err(invalid_params("attachmentType must not be empty")); + } + if attachment_type.len() > MAX_THREAD_ATTACHMENT_TYPE_BYTES { + return Err(invalid_params(format!( + "attachmentType must not exceed {MAX_THREAD_ATTACHMENT_TYPE_BYTES} bytes" + ))); + } + if identity_key.trim().is_empty() { + return Err(invalid_params("identityKey must not be empty")); + } + if identity_key.len() > MAX_THREAD_ATTACHMENT_IDENTITY_KEY_BYTES { + return Err(invalid_params(format!( + "identityKey must not exceed {MAX_THREAD_ATTACHMENT_IDENTITY_KEY_BYTES} bytes" + ))); + } + Ok(()) +} + +fn parse_attachment_thread_id(thread_id: &str) -> Result { + ThreadId::from_string(thread_id) + .map_err(|error| invalid_params(format!("invalid thread id: {error}"))) +} + +fn api_thread_attachment(attachment: StoredThreadAttachment) -> ThreadAttachment { + ThreadAttachment { + id: attachment.id, + attachment_type: attachment.attachment_type, + identity_key: attachment.identity_key, + payload: attachment.payload, + created_at: attachment.created_at, + } +} + +fn unsupported_thread_attachment_operation(operation: &'static str) -> JSONRPCErrorError { + method_not_found(format!( + "{operation} is not supported by the configured thread store" + )) +} + +fn thread_attachment_store_error( + operation: &'static str, + error: ThreadStoreError, +) -> JSONRPCErrorError { + match error { + ThreadStoreError::Unsupported { .. } => unsupported_thread_attachment_operation(operation), + ThreadStoreError::InvalidRequest { message } => invalid_params(message), + ThreadStoreError::ThreadNotFound { thread_id } => { + invalid_params(format!("thread not found: {thread_id}")) + } + error @ (ThreadStoreError::Conflict { .. } | ThreadStoreError::Internal { .. }) => { + internal_error(format!("failed to process {operation}: {error}")) + } + } +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 67fe1b9541..fab78952c9 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -105,6 +105,7 @@ mod session_end; mod skills_list; mod sleep; mod thread_archive; +mod thread_attachments; mod thread_delete; mod thread_environments; mod thread_fork; diff --git a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs index 0ebf3024cd..abf69a853a 100644 --- a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs +++ b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs @@ -31,6 +31,9 @@ use codex_app_server_protocol::InitializeParams; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::ThreadAttachmentAddParams; +use codex_app_server_protocol::ThreadAttachmentListParams; +use codex_app_server_protocol::ThreadAttachmentRemoveParams; use codex_app_server_protocol::ThreadDeleteParams; use codex_app_server_protocol::ThreadDeleteResponse; use codex_app_server_protocol::ThreadHistoryMode; @@ -187,6 +190,58 @@ async fn thread_start_defaults_to_legacy_without_history_list_support() -> Resul Ok(()) } +#[tokio::test] +async fn thread_attachment_operations_without_sqlite_return_method_not_found() -> Result<()> { + let codex_home = TempDir::new()?; + let store_id = Uuid::new_v4().to_string(); + create_config_toml_with_thread_store(codex_home.path(), "http://127.0.0.1:1", &store_id)?; + let _in_memory_store = InMemoryThreadStoreId { store_id }; + let client = start_in_process_server(codex_home.path()).await?; + + for request in [ + ClientRequest::ThreadAttachmentAdd { + request_id: RequestId::Integer(1), + params: ThreadAttachmentAddParams { + thread_id: "not-a-thread-id".to_string(), + attachment_type: " ".to_string(), + identity_key: " ".to_string(), + payload: serde_json::json!({}), + }, + }, + ClientRequest::ThreadAttachmentList { + request_id: RequestId::Integer(2), + params: ThreadAttachmentListParams { + thread_id: uuid::Uuid::now_v7().to_string(), + cursor: None, + limit: None, + }, + }, + ClientRequest::ThreadAttachmentRemove { + request_id: RequestId::Integer(3), + params: ThreadAttachmentRemoveParams { + thread_id: "not-a-thread-id".to_string(), + attachment_type: " ".to_string(), + identity_key: " ".to_string(), + }, + }, + ] { + let method = request.method_name(); + let error = client + .request(request) + .await? + .expect_err("attachment management is unsupported by this store"); + assert_eq!(error.code, -32601); + assert_eq!( + error.message, + format!("{method} is not supported by the configured thread store") + ); + } + + client.shutdown().await?; + assert_no_local_persistence_artifacts(codex_home.path())?; + Ok(()) +} + #[tokio::test] async fn thread_start_rejects_paginated_history_without_list_support() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/app-server/tests/suite/v2/thread_attachments.rs b/codex-rs/app-server/tests/suite/v2/thread_attachments.rs new file mode 100644 index 0000000000..85e5c1919e --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/thread_attachments.rs @@ -0,0 +1,506 @@ +//! Public RPC coverage for durable thread attachment membership and notifications. + +use super::connection_handling_websocket::DEFAULT_READ_TIMEOUT; +use super::connection_handling_websocket::connect_websocket; +use super::connection_handling_websocket::read_jsonrpc_message; +use super::connection_handling_websocket::read_notification_for_method; +use super::connection_handling_websocket::read_response_for_id; +use super::connection_handling_websocket::send_initialize_request; +use super::connection_handling_websocket::send_request; +use super::connection_handling_websocket::spawn_websocket_server; +use anyhow::Context; +use anyhow::Result; +use app_test_support::MockResponsesConfig; +use app_test_support::TestAppServer; +use app_test_support::create_fake_rollout; +use app_test_support::create_mock_responses_server_repeating_assistant; +use app_test_support::to_response; +use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::JSONRPCError; +use codex_app_server_protocol::JSONRPCMessage; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadAttachment; +use codex_app_server_protocol::ThreadAttachmentAddOutcome; +use codex_app_server_protocol::ThreadAttachmentAddParams; +use codex_app_server_protocol::ThreadAttachmentAddResponse; +use codex_app_server_protocol::ThreadAttachmentListParams; +use codex_app_server_protocol::ThreadAttachmentListResponse; +use codex_app_server_protocol::ThreadAttachmentOperation; +use codex_app_server_protocol::ThreadAttachmentRemoveParams; +use codex_app_server_protocol::ThreadAttachmentRemoveResponse; +use codex_app_server_protocol::ThreadAttachmentUpdatedNotification; +use codex_app_server_protocol::ThreadListResponse; +use codex_app_server_protocol::ThreadLoadedListParams; +use codex_app_server_protocol::ThreadLoadedListResponse; +use codex_features::Feature; +use pretty_assertions::assert_eq; +use serde_json::json; +use tempfile::TempDir; +use tokio::time::timeout; + +#[tokio::test] +async fn thread_attachments_support_unloaded_listing_and_idempotent_attachment() -> Result<()> { + let responses = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + MockResponsesConfig::new(&responses.uri()) + .enable_feature(Feature::Sqlite) + .write(codex_home.path())?; + let first_thread = create_fake_rollout( + codex_home.path(), + "2025-01-06T08-00-00", + "2025-01-06T08:00:00Z", + "First thread", + Some("mock_provider"), + /*git_info*/ None, + )?; + let unrelated_thread = create_fake_rollout( + codex_home.path(), + "2025-01-06T10-00-00", + "2025-01-06T10:00:00Z", + "Unrelated thread", + Some("mock_provider"), + /*git_info*/ None, + )?; + let mut server = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .build_initialized() + .await?; + let request_id = server + .send_raw_request("thread/list", Some(json!({ "limit": 20 }))) + .await?; + let _: ThreadListResponse = + timeout(DEFAULT_READ_TIMEOUT, server.read_response(request_id)).await??; + + let loaded: ThreadLoadedListResponse = server + .request(|request_id| ClientRequest::ThreadLoadedList { + request_id, + params: ThreadLoadedListParams::default(), + }) + .await?; + assert_eq!(loaded.data, Vec::::new()); + + let first_params = ThreadAttachmentAddParams { + thread_id: first_thread.clone(), + attachment_type: "pull_request".to_string(), + identity_key: r#"["github.com","openai","codex",123]"#.to_string(), + payload: json!({ "url": "https://github.com/openai/codex/pull/123" }), + }; + let first: ThreadAttachmentAddResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentAdd { + request_id, + params: first_params.clone(), + }) + .await?; + assert_eq!(first.outcome, ThreadAttachmentAddOutcome::Created); + let first_attachment = first.attachment; + let created_notification: ThreadAttachmentUpdatedNotification = timeout( + DEFAULT_READ_TIMEOUT, + server.read_notification("thread/attachment/updated"), + ) + .await??; + assert_eq!( + created_notification, + ThreadAttachmentUpdatedNotification { + thread_id: first_thread.clone(), + attachment_type: first_attachment.attachment_type.clone(), + identity_key: first_attachment.identity_key.clone(), + attachment_id: first_attachment.id.clone(), + operation: ThreadAttachmentOperation::Created, + } + ); + + let duplicate: ThreadAttachmentAddResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentAdd { + request_id, + params: first_params, + }) + .await?; + assert_eq!( + duplicate, + ThreadAttachmentAddResponse { + outcome: ThreadAttachmentAddOutcome::Existing, + attachment: first_attachment.clone(), + } + ); + + let second: ThreadAttachmentAddResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentAdd { + request_id, + params: ThreadAttachmentAddParams { + thread_id: first_thread.clone(), + attachment_type: "pull_request".to_string(), + identity_key: r#"["github.com","openai","codex",456]"#.to_string(), + payload: json!({ "url": "https://github.com/openai/codex/pull/456" }), + }, + }) + .await?; + let second_attachment = second.attachment; + + let unrelated: ThreadAttachmentAddResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentAdd { + request_id, + params: ThreadAttachmentAddParams { + thread_id: unrelated_thread, + attachment_type: "pull_request".to_string(), + identity_key: r#"["github.com","openai","codex",789]"#.to_string(), + payload: json!({ "url": "https://github.com/openai/codex/pull/789" }), + }, + }) + .await?; + assert_eq!(unrelated.outcome, ThreadAttachmentAddOutcome::Created); + + let first_page: ThreadAttachmentListResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentList { + request_id, + params: ThreadAttachmentListParams { + thread_id: first_thread.clone(), + cursor: None, + limit: Some(1), + }, + }) + .await?; + assert_eq!(first_page.data.len(), 1); + let second_page: ThreadAttachmentListResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentList { + request_id, + params: ThreadAttachmentListParams { + thread_id: first_thread.clone(), + cursor: first_page.next_cursor.clone(), + limit: Some(1), + }, + }) + .await?; + assert_eq!(second_page.next_cursor, None); + + let mut actual = first_page.data; + actual.extend(second_page.data); + let expected = vec![first_attachment, second_attachment]; + assert_eq!(actual, expected); + + let empty: ThreadAttachmentListResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentList { + request_id, + params: ThreadAttachmentListParams { + thread_id: uuid::Uuid::now_v7().to_string(), + cursor: None, + limit: Some(10), + }, + }) + .await?; + assert_eq!(empty.data, Vec::::new()); + Ok(()) +} + +#[tokio::test] +async fn attachment_removal_is_idempotent_and_allows_reattachment() -> Result<()> { + let responses = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + MockResponsesConfig::new(&responses.uri()) + .enable_feature(Feature::Sqlite) + .write(codex_home.path())?; + let thread_id = create_fake_rollout( + codex_home.path(), + "2025-01-06T08-00-00", + "2025-01-06T08:00:00Z", + "Stored thread", + Some("mock_provider"), + /*git_info*/ None, + )?; + let mut server = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .build_initialized() + .await?; + let request_id = server + .send_raw_request("thread/list", Some(json!({ "limit": 20 }))) + .await?; + let _: ThreadListResponse = + timeout(DEFAULT_READ_TIMEOUT, server.read_response(request_id)).await??; + + let create_params = ThreadAttachmentAddParams { + thread_id: thread_id.clone(), + attachment_type: "pull_request".to_string(), + identity_key: r#"["github.com","openai","codex",123]"#.to_string(), + payload: json!({ "url": "https://github.com/openai/codex/pull/123" }), + }; + let created: ThreadAttachmentAddResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentAdd { + request_id, + params: create_params.clone(), + }) + .await?; + assert_eq!(created.outcome, ThreadAttachmentAddOutcome::Created); + let created_notification: ThreadAttachmentUpdatedNotification = timeout( + DEFAULT_READ_TIMEOUT, + server.read_notification("thread/attachment/updated"), + ) + .await??; + assert_eq!(created_notification.attachment_id, created.attachment.id); + + let delete_params = ThreadAttachmentRemoveParams { + thread_id: thread_id.to_ascii_uppercase(), + attachment_type: create_params.attachment_type.clone(), + identity_key: create_params.identity_key.clone(), + }; + let _: ThreadAttachmentRemoveResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentRemove { + request_id, + params: delete_params.clone(), + }) + .await?; + let removed_notification: ThreadAttachmentUpdatedNotification = timeout( + DEFAULT_READ_TIMEOUT, + server.read_notification("thread/attachment/updated"), + ) + .await??; + assert_eq!( + removed_notification, + ThreadAttachmentUpdatedNotification { + thread_id: thread_id.clone(), + attachment_type: delete_params.attachment_type.clone(), + identity_key: delete_params.identity_key.clone(), + attachment_id: created.attachment.id, + operation: ThreadAttachmentOperation::Deleted, + } + ); + + let _: ThreadAttachmentRemoveResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentRemove { + request_id, + params: delete_params, + }) + .await?; + + let listed: ThreadAttachmentListResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentList { + request_id, + params: ThreadAttachmentListParams { + thread_id, + cursor: None, + limit: Some(10), + }, + }) + .await?; + assert_eq!(listed.data, Vec::::new()); + + let restored: ThreadAttachmentAddResponse = server + .request(|request_id| ClientRequest::ThreadAttachmentAdd { + request_id, + params: create_params, + }) + .await?; + assert_eq!(restored.outcome, ThreadAttachmentAddOutcome::Created); + let restored_notification: ThreadAttachmentUpdatedNotification = timeout( + DEFAULT_READ_TIMEOUT, + server.read_notification("thread/attachment/updated"), + ) + .await??; + assert_eq!( + restored_notification.operation, + ThreadAttachmentOperation::Created + ); + assert_eq!(restored_notification.attachment_id, restored.attachment.id); + Ok(()) +} + +#[tokio::test] +async fn thread_attachment_requests_reject_invalid_identities_and_cursors() -> Result<()> { + let responses = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + MockResponsesConfig::new(&responses.uri()) + .enable_feature(Feature::Sqlite) + .write(codex_home.path())?; + let mut server = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .build_initialized() + .await?; + + for (method, params) in [ + ( + "thread/attachment/add", + json!({ + "threadId": "not-a-thread-id", + "attachmentType": "pull_request", + "identityKey": r#"["github.com","openai","codex",123]"#, + "payload": {} + }), + ), + ( + "thread/attachment/add", + json!({ + "threadId": uuid::Uuid::now_v7().to_string(), + "attachmentType": " ", + "identityKey": r#"["github.com","openai","codex",123]"#, + "payload": {} + }), + ), + ( + "thread/attachment/remove", + json!({ + "threadId": uuid::Uuid::now_v7().to_string(), + "attachmentType": "pull_request", + "identityKey": " " + }), + ), + ("thread/attachment/list", json!({ "threadId": "invalid" })), + ( + "thread/attachment/list", + json!({ + "threadId": uuid::Uuid::now_v7().to_string(), + "cursor": "invalid-cursor" + }), + ), + ] { + let request_id = server.send_raw_request(method, Some(params)).await?; + let error: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + server.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!(error.error.code, -32602); + } + Ok(()) +} + +#[tokio::test] +async fn thread_attachment_mutations_respond_before_broadcasting_updates_to_multiple_clients() +-> Result<()> { + let responses = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + MockResponsesConfig::new(&responses.uri()) + .enable_feature(Feature::Sqlite) + .write(codex_home.path())?; + let thread_id = create_fake_rollout( + codex_home.path(), + "2025-01-06T08-00-00", + "2025-01-06T08:00:00Z", + "Stored thread", + Some("mock_provider"), + /*git_info*/ None, + )?; + let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?; + + let result = async { + let mut first_client = connect_websocket(bind_addr).await?; + let mut second_client = connect_websocket(bind_addr).await?; + send_initialize_request(&mut first_client, /*id*/ 1, "attachment-client-one").await?; + timeout( + DEFAULT_READ_TIMEOUT, + read_response_for_id(&mut first_client, /*id*/ 1), + ) + .await??; + send_initialize_request(&mut second_client, /*id*/ 2, "attachment-client-two").await?; + timeout( + DEFAULT_READ_TIMEOUT, + read_response_for_id(&mut second_client, /*id*/ 2), + ) + .await??; + + send_request( + &mut first_client, + "thread/list", + /*id*/ 3, + Some(json!({ "limit": 20 })), + ) + .await?; + let _: ThreadListResponse = + to_response(read_response_for_id(&mut first_client, /*id*/ 3).await?)?; + + send_request( + &mut first_client, + "thread/attachment/add", + /*id*/ 4, + Some(json!({ + "threadId": thread_id.to_uppercase(), + "attachmentType": "pull_request", + "identityKey": r#"["github.com","openai","codex",123]"#, + "payload": { "url": "https://github.com/openai/codex/pull/123" } + })), + ) + .await?; + let first_message = read_jsonrpc_message(&mut first_client).await?; + let JSONRPCMessage::Response(response) = first_message else { + anyhow::bail!("attachment creation must respond before notifying the requester"); + }; + assert_eq!(response.id, RequestId::Integer(4)); + let created: ThreadAttachmentAddResponse = to_response(response)?; + let attachment = created.attachment; + let first_notification = + read_notification_for_method(&mut first_client, "thread/attachment/updated").await?; + let first_notification: ThreadAttachmentUpdatedNotification = serde_json::from_value( + first_notification + .params + .context("attachment notification")?, + )?; + let second_notification = + read_notification_for_method(&mut second_client, "thread/attachment/updated").await?; + let second_notification: ThreadAttachmentUpdatedNotification = serde_json::from_value( + second_notification + .params + .context("attachment notification")?, + )?; + + assert_eq!( + first_notification, + ThreadAttachmentUpdatedNotification { + thread_id: thread_id.clone(), + attachment_type: "pull_request".to_string(), + identity_key: r#"["github.com","openai","codex",123]"#.to_string(), + attachment_id: attachment.id.clone(), + operation: ThreadAttachmentOperation::Created, + } + ); + assert_eq!(first_notification, second_notification); + + send_request( + &mut second_client, + "thread/attachment/remove", + /*id*/ 5, + Some(json!({ + "threadId": format!("{{{thread_id}}}"), + "attachmentType": "pull_request", + "identityKey": r#"["github.com","openai","codex",123]"# + })), + ) + .await?; + let first_message = read_jsonrpc_message(&mut second_client).await?; + let JSONRPCMessage::Response(response) = first_message else { + anyhow::bail!("attachment deletion must respond before notifying the requester"); + }; + assert_eq!(response.id, RequestId::Integer(5)); + let _: ThreadAttachmentRemoveResponse = to_response(response)?; + + let first_notification = + read_notification_for_method(&mut first_client, "thread/attachment/updated").await?; + let first_notification: ThreadAttachmentUpdatedNotification = serde_json::from_value( + first_notification + .params + .context("attachment notification")?, + )?; + let second_notification = + read_notification_for_method(&mut second_client, "thread/attachment/updated").await?; + let second_notification: ThreadAttachmentUpdatedNotification = serde_json::from_value( + second_notification + .params + .context("attachment notification")?, + )?; + assert_eq!( + first_notification, + ThreadAttachmentUpdatedNotification { + thread_id, + attachment_type: "pull_request".to_string(), + identity_key: r#"["github.com","openai","codex",123]"#.to_string(), + attachment_id: attachment.id, + operation: ThreadAttachmentOperation::Deleted, + } + ); + assert_eq!(first_notification, second_notification); + Ok(()) + } + .await; + + process + .kill() + .await + .context("failed to stop websocket app-server process")?; + result +} diff --git a/codex-rs/tui/src/app/app_server_event_targets.rs b/codex-rs/tui/src/app/app_server_event_targets.rs index 27359fd5c5..0054f79248 100644 --- a/codex-rs/tui/src/app/app_server_event_targets.rs +++ b/codex-rs/tui/src/app/app_server_event_targets.rs @@ -59,6 +59,9 @@ pub(super) fn server_notification_thread_target( ServerNotification::ThreadNameUpdated(notification) => { Some(notification.thread_id.as_str()) } + ServerNotification::ThreadAttachmentUpdated(notification) => { + Some(notification.thread_id.as_str()) + } ServerNotification::ThreadProjectUpdated(notification) => { Some(notification.thread_id.as_str()) } @@ -231,6 +234,8 @@ mod tests { use codex_app_server_protocol::McpServerStartupState; use codex_app_server_protocol::McpServerStatusUpdatedNotification; use codex_app_server_protocol::ServerNotification; + use codex_app_server_protocol::ThreadAttachmentOperation; + use codex_app_server_protocol::ThreadAttachmentUpdatedNotification; use codex_app_server_protocol::ThreadSettings; use codex_app_server_protocol::ThreadSettingsUpdatedNotification; use codex_app_server_protocol::WarningNotification; @@ -352,4 +357,21 @@ mod tests { assert_eq!(target, ServerNotificationThreadTarget::Thread(thread_id)); } + + #[test] + fn thread_attachment_updated_notifications_route_to_threads() { + let thread_id = ThreadId::new(); + let notification = + ServerNotification::ThreadAttachmentUpdated(ThreadAttachmentUpdatedNotification { + thread_id: thread_id.to_string(), + attachment_type: "pull_request".to_string(), + identity_key: r#"["github.com","openai","codex",123]"#.to_string(), + attachment_id: "attachment-1".to_string(), + operation: ThreadAttachmentOperation::Deleted, + }); + + let target = server_notification_thread_target(¬ification); + + assert_eq!(target, ServerNotificationThreadTarget::Thread(thread_id)); + } } diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 6da4b14a93..1cb3cf299f 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -135,6 +135,8 @@ use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadArchivedNotification; +use codex_app_server_protocol::ThreadAttachmentOperation; +use codex_app_server_protocol::ThreadAttachmentUpdatedNotification; use codex_app_server_protocol::ThreadClosedNotification; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadSettings; @@ -1976,7 +1978,8 @@ async fn archived_untracked_threads_do_not_appear_in_agent_picker() -> Result<() app.chat_widget.config_ref(), )) .await?; - let primary_thread_id = ThreadId::new(); + let primary_thread_id = + ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id"); app.enqueue_primary_thread_session( test_thread_session(primary_thread_id, test_path_buf("/tmp/project")), Vec::new(), @@ -1996,8 +1999,32 @@ async fn archived_untracked_threads_do_not_appear_in_agent_picker() -> Result<() assert!(!app.thread_event_channels.contains_key(&archived_thread_id)); + let attachment_thread_id = ThreadId::new(); + app.handle_app_server_event( + &app_server, + codex_app_server_client::AppServerEvent::ServerNotification(Box::new( + ServerNotification::ThreadAttachmentUpdated(ThreadAttachmentUpdatedNotification { + thread_id: attachment_thread_id.to_string(), + attachment_type: "pull_request".to_string(), + identity_key: r#"["github.com","openai","codex",123]"#.to_string(), + attachment_id: "attachment-1".to_string(), + operation: ThreadAttachmentOperation::Deleted, + }), + )), + ) + .await; + + assert!( + !app.thread_event_channels + .contains_key(&attachment_thread_id) + ); + Box::pin(app.open_agent_picker(&mut app_server)).await; + assert_app_snapshot!( + "untracked_thread_notifications_agent_picker", + render_bottom_popup(&app.chat_widget, /*width*/ 80) + ); assert_eq!( app.agent_navigation.ordered_thread_ids(), vec![primary_thread_id] diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index e80695fe40..f0d15b2aeb 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -282,6 +282,7 @@ impl ThreadEventStore { | ServerNotification::FileChangePatchUpdated(_) | ServerNotification::ServerRequestResolved(_) | ServerNotification::McpToolCallProgress(_) + | ServerNotification::ThreadAttachmentUpdated(_) | ServerNotification::ThreadRealtimeItemAdded(_) | ServerNotification::ThreadRealtimeOutputAudioDelta(_) | ServerNotification::ThreadRealtimeSdp(_) @@ -646,6 +647,8 @@ mod tests { use codex_app_server_protocol::McpToolCallProgressNotification; use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification; use codex_app_server_protocol::RequestId as AppServerRequestId; + use codex_app_server_protocol::ThreadAttachmentOperation; + use codex_app_server_protocol::ThreadAttachmentUpdatedNotification; use codex_app_server_protocol::ThreadRealtimeAudioChunk; use codex_app_server_protocol::ThreadRealtimeOutputAudioDeltaNotification; use codex_app_server_protocol::TurnCompletedNotification; @@ -902,6 +905,15 @@ mod tests { let large_payload = "x".repeat(1024 * 1024); for _ in 0..32 { + store.push_notification_ref(&ServerNotification::ThreadAttachmentUpdated( + ThreadAttachmentUpdatedNotification { + thread_id: thread_id.to_string(), + attachment_type: "pullRequest".to_string(), + identity_key: "openai/codex#1".to_string(), + attachment_id: "attachment-1".to_string(), + operation: ThreadAttachmentOperation::Created, + }, + )); store.push_notification_ref(&ServerNotification::McpToolCallProgress( McpToolCallProgressNotification { thread_id: thread_id.to_string(), diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 4d9ecab82c..d9ea5f126b 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -1166,7 +1166,9 @@ impl App { } if matches!( notification, - ServerNotification::ThreadSettingsUpdated(_) | ServerNotification::ThreadArchived(_) + ServerNotification::ThreadSettingsUpdated(_) + | ServerNotification::ThreadArchived(_) + | ServerNotification::ThreadAttachmentUpdated(_) ) && self.primary_thread_id.is_some() && self.primary_thread_id != Some(thread_id) && !self.thread_event_channels.contains_key(&thread_id) diff --git a/codex-rs/tui/src/chatwidget/protocol.rs b/codex-rs/tui/src/chatwidget/protocol.rs index ca893999f9..9ec441ad2e 100644 --- a/codex-rs/tui/src/chatwidget/protocol.rs +++ b/codex-rs/tui/src/chatwidget/protocol.rs @@ -313,6 +313,7 @@ impl ChatWidget { | ServerNotification::ThreadArchived(_) | ServerNotification::ThreadDeleted(_) | ServerNotification::ThreadUnarchived(_) + | ServerNotification::ThreadAttachmentUpdated(_) | ServerNotification::RawResponseItemCompleted(_) | ServerNotification::RawResponseCompleted(_) | ServerNotification::CommandExecOutputDelta(_) diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__tests__untracked_thread_notifications_agent_picker.snap b/codex-rs/tui/src/snapshots/codex_tui__app__tests__untracked_thread_notifications_agent_picker.snap new file mode 100644 index 0000000000..d15daf8957 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__app__tests__untracked_thread_notifications_agent_picker.snap @@ -0,0 +1,10 @@ +--- +source: tui/src/app/tests.rs +expression: "render_bottom_popup(&app.chat_widget, 80)" +--- + Subagents + Select an agent to watch. ⌥ + ← previous, ⌥ + → next. + +› 1. • Main [default] (current) 00000000-0000-0000-0000-000000000123 + + Press enter to confirm or esc to go back diff --git a/sdk/python/src/openai_codex/generated/notification_registry.py b/sdk/python/src/openai_codex/generated/notification_registry.py index 4b2af11c8a..2fbb5048f8 100644 --- a/sdk/python/src/openai_codex/generated/notification_registry.py +++ b/sdk/python/src/openai_codex/generated/notification_registry.py @@ -54,6 +54,7 @@ from .v2_all import SkillsChangedNotification from .v2_all import StrictReviewRequiredNotification from .v2_all import TerminalInteractionNotification from .v2_all import ThreadArchivedNotification +from .v2_all import ThreadAttachmentUpdatedNotification from .v2_all import ThreadClosedNotification from .v2_all import ThreadDeletedNotification from .v2_all import ThreadGoalClearedNotification @@ -135,6 +136,7 @@ KnownNotificationPayload: TypeAlias = ( | StrictReviewRequiredNotification | TerminalInteractionNotification | ThreadArchivedNotification + | ThreadAttachmentUpdatedNotification | ThreadClosedNotification | ThreadDeletedNotification | ThreadGoalClearedNotification @@ -216,6 +218,7 @@ NOTIFICATION_MODELS: dict[str, type[KnownNotificationPayload]] = { "serverRequest/resolved": ServerRequestResolvedNotification, "skills/changed": SkillsChangedNotification, "thread/archived": ThreadArchivedNotification, + "thread/attachment/updated": ThreadAttachmentUpdatedNotification, "thread/closed": ThreadClosedNotification, "thread/compacted": ContextCompactedNotification, "thread/deleted": ThreadDeletedNotification, diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index 3189e26589..ca37902c63 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -5034,6 +5034,89 @@ class ThreadArchivedNotification(BaseModel): thread_id: Annotated[str, Field(alias="threadId")] +class ThreadAttachment(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + attachment_type: Annotated[str, Field(alias="attachmentType")] + created_at: Annotated[int, Field(alias="createdAt")] + id: str + identity_key: Annotated[str, Field(alias="identityKey")] + payload: Any + + +class ThreadAttachmentAddOutcome(Enum): + created = "created" + existing = "existing" + + +class ThreadAttachmentAddParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + attachment_type: Annotated[str, Field(alias="attachmentType")] + identity_key: Annotated[str, Field(alias="identityKey")] + payload: Any + thread_id: Annotated[str, Field(alias="threadId")] + + +class ThreadAttachmentAddResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + attachment: ThreadAttachment + outcome: ThreadAttachmentAddOutcome + + +class ThreadAttachmentListParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cursor: str | None = None + limit: Annotated[int | None, Field(ge=0)] = None + thread_id: Annotated[str, Field(alias="threadId")] + + +class ThreadAttachmentListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: list[ThreadAttachment] + next_cursor: Annotated[str | None, Field(alias="nextCursor")] = None + + +class ThreadAttachmentOperation(Enum): + created = "created" + deleted = "deleted" + + +class ThreadAttachmentRemoveParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + attachment_type: Annotated[str, Field(alias="attachmentType")] + identity_key: Annotated[str, Field(alias="identityKey")] + thread_id: Annotated[str, Field(alias="threadId")] + + +class ThreadAttachmentRemoveResponse(BaseModel): + pass + model_config = ConfigDict( + populate_by_name=True, + ) + + +class ThreadAttachmentUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + attachment_id: Annotated[str, Field(alias="attachmentId")] + attachment_type: Annotated[str, Field(alias="attachmentType")] + identity_key: Annotated[str, Field(alias="identityKey")] + operation: ThreadAttachmentOperation + thread_id: Annotated[str, Field(alias="threadId")] + + class ThreadClosedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6638,6 +6721,39 @@ class ThreadMetadataUpdateRequest(BaseModel): params: ThreadMetadataUpdateParams +class ThreadAttachmentAddRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["thread/attachment/add"], Field(title="Thread/attachment/addRequestMethod") + ] + params: ThreadAttachmentAddParams + + +class ThreadAttachmentListRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["thread/attachment/list"], Field(title="Thread/attachment/listRequestMethod") + ] + params: ThreadAttachmentListParams + + +class ThreadAttachmentRemoveRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["thread/attachment/remove"], Field(title="Thread/attachment/removeRequestMethod") + ] + params: ThreadAttachmentRemoveParams + + class ThreadSectionMoveRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -8776,6 +8892,24 @@ class ThreadNameUpdatedServerNotification(BaseModel): params: ThreadNameUpdatedNotification +class ThreadAttachmentUpdatedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + emitted_at_ms: Annotated[ + int | None, + Field( + alias="emittedAtMs", + description="Unix timestamp (in milliseconds) when app-server emitted this notification.", + ), + ] = None + method: Annotated[ + Literal["thread/attachment/updated"], + Field(title="Thread/attachment/updatedNotificationMethod"), + ] + params: ThreadAttachmentUpdatedNotification + + class ThreadGoalClearedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -11957,6 +12091,9 @@ class ClientRequest( | ThreadGoalGetRequest | ThreadGoalClearRequest | ThreadMetadataUpdateRequest + | ThreadAttachmentAddRequest + | ThreadAttachmentListRequest + | ThreadAttachmentRemoveRequest | ThreadSectionMoveRequest | ThreadUnarchiveRequest | ThreadCompactStartRequest @@ -12062,6 +12199,9 @@ class ClientRequest( | ThreadGoalGetRequest | ThreadGoalClearRequest | ThreadMetadataUpdateRequest + | ThreadAttachmentAddRequest + | ThreadAttachmentListRequest + | ThreadAttachmentRemoveRequest | ThreadSectionMoveRequest | ThreadUnarchiveRequest | ThreadCompactStartRequest @@ -12336,6 +12476,7 @@ class ServerNotification( | ThreadRevertedServerNotification | SkillsChangedServerNotification | ThreadNameUpdatedServerNotification + | ThreadAttachmentUpdatedServerNotification | ThreadGoalUpdatedServerNotification | ThreadGoalClearedServerNotification | ThreadQueueChangedServerNotification @@ -12423,6 +12564,7 @@ class ServerNotification( | ThreadRevertedServerNotification | SkillsChangedServerNotification | ThreadNameUpdatedServerNotification + | ThreadAttachmentUpdatedServerNotification | ThreadGoalUpdatedServerNotification | ThreadGoalClearedServerNotification | ThreadQueueChangedServerNotification